Transcript 幻灯片 1

第十三章 网络通信与因特网应用程序
由于因特网的的出现和迅速盛行,基于网络通信,越来越多
的应用程序的运行环境不再是同地的单机系统,而是通过网络
(包括局域网和因特网)连接起来的异地的多机系统。从
Windows NT 和 Windows 95 开始,微软把网络功能逐步地融入到
其操作系统之中,使得网络功能在各类应用程序中,根据功能
需要完成不同程度和不同需要的网络任务已不是新鲜话题。例
如,对一个 Web 站点中运行的应用程序进行核实,判断是否已
经更新,并提示用户是否更新程序版本;又如,网络游戏程序
允许玩家与异地的玩家直接对阵,而不再是只能与游戏程序对
阵;等等。
13.1 网络通信的工作原理
应用程序可以有许多网络功能,实现这些功能的基础是网络
通信,Windows 平台通过 Winsock 接口支持网络通信。MFC 的
CAsyncSocket 和 CSocket 为使用 Winsock 接口编程提供了方便。
因此,理解并学会如何使用 Winsock 接口和 MFC Winsock 类进行
编程是本章的学习重点之一,主要的内容包括:
·应用程序如何使用 Winsock 接口在两个以上计算机之间进行
网络通信。
·客户机和服务器应用程序间的区别以及它们在建立通信连接
中各自所起的作用。
·MFC Winsock 类如何简化编写因特网应用程序的过程。
·怎样创建从 MFC Winsock 类派生的自定义 Winsock 类,以便创
建事件驱动的网络应用程序。
大多数通过网络进行通信的应用程序,不论是通过因特网还
是小型的局部网络,它们都使用同样的原则和功能来执行网络
通信。运行在两台计算机上(也包括运行一台计算机上)的两
个应用程序之间通过网络的通信过程与日常生活中通过电话的
通信非常相似。
请求连接
接受连接请求
双向发送消息
客户机
服务器(侦听连接)
通信的基本原理和过程如下:
1 侦听
通信连接的接收方 —— 服务器上的应用程序必须首先运行
对申请的连接进行侦听。这就像电话通信中接电话的一方有人
接听是通信成功的前提。
2 请求连接
通信连接的申请方 —— 客户机上的应用程序运行并发出与
服务器上应用程序连接的请求,以便进行通信。这就像电话通
信中打电话的一方拨打接话方的电话。
3 接受连接
通信连接的接收方 —— 服务器上的应用程序收到连接请
求,并接受连接成功建立通信连接。这就像电话通信中接电话
的一方听电话铃声,并接听来电。
4 通信
通信连接一旦成功建立,客户机应用程序和服务器应用程序
之间信息的发送和接收便可以自由进行。这就像电话通信中通
话双方的交谈过程。
5 通信终止
如果进行通信的应用程序的一方或双方完成了交互就可以关
闭(切断)连接。这就像电话通信中,在打完电话后把电话挂
断一样。如果连接由一方关闭(电话挂断),另一方会检测到
并关闭(挂断)自己的一方;或者双方由于其他原因连接被终
止。
注意:以上是对使用 TCP/IP 协议(因特网上的主要网络协议)
的网络通信的工作原理的基本描述。很多其他网络协议则对上
述描述做了一些微妙的改变。如 UDP 协议,更像无线广播,在
这种协议中,两个应用程序之间无须连接的建立;如果一个应
用程序发送了消息,而另一个应用程序负责确保它能收到了所
有消息。这些协议比我们所说的要复杂得多。
13.1.1 报路、端口和地址
在应用程序中,用于执行大多数网络通信的基本对象称作报
路。报路首先是在伯克利大学的 UNIX 系统上开发出来的。当时
设计报路的目的是为了使得应用程序能够以它们读写文件的同
样方式来完成网络通信。尽管自那之后报路技术已有了很大的
发展,但基本工作原理仍然没变。
在 Windows 95 之前,网络功能还没有加入到 Windows 操作系
统,为了实现网络通信需要从不同公司购买通信所需要的网络
协议。这些公司对于应用程序完成网络通信都有各自不同的解
决方法。结果使得执行网络通信的应用程序需要与不同的网络
软件打交道,非常不便。迫使包括微软在内的所有涉及网络的
公司共同开发了 WinSock (Windows Socket)API,为应用程序开
发提供一致的接口,用以实现所有网络通信功能。
与创建并打开一个文件对象必须知道文件名及其位置相似,
创建并打开一个用于网络通信的报路必须知道需要进行通信的
计算机地址以及它所侦听的端口(一个指定的计算机地址和一
个指定端口的组合成为套接字(Socket) 确定一个指定的通信
报路)。端口可比作一个电话分机,而计算机地址则是电话总
机号码。如同先拨打电话主机号码,再拨分机号码打电话一
样,端口用于路由网络通信,如下图所示:
计算机中的网络接口使用不
同套接字确定的报路把网络
消息指引到正确的应用程序
Port100
网络应用程序
Port150
网络应用程序
Port200
网络应用程序
Port50
网络应用程序
Port4000
网络应用程序
Port801
网络应用程序
网络接口
如果指定了错误的套接字(计算机地址或端口),可能会连
接到一个不同的应用程序上;如果对方没有侦听地址的应用程
序,则请求方应用程序不可能得到任何响应。
注意:每次只能有一个应用程序侦听一台计算机的任一特定端
口。尽管同时可以有许多应用程序侦听一台计算机的多个请
求,但它们必须在不同的端口上侦听。
13.1.2 创建一个报路
在用 Visual C++ 建立应用程序时,可以直接使用 WinSock API
或 MFC Winsock 类为应用程序添加网络通讯能力。MFC Winsock
类CAsyncSocket 提供了完整的、事件驱动的报路通信。因此你
可以定义从该类派生的自定义类来捕获和响应这些事件。在应
用程序中创建一个报路需要做如下工作:
1 在创建应用程序项目中,选择 Windows Sockets 支持。这一选
择会在项目的预编译头文件 stdafx.h 中添加相应的支持语句:
#endif // _AFX_NO_AFXCMN_SUPPORT
#include <afxsock.h>
// MFC socket extensions
//{{AFX_INSERT_LOCATION}}
并在应用程序类的实例初始化成员函数 InitInstance 中添加支
持使用 Windows Sockets 的初始化语句如下:
BOOL CSockApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
AfxEnableControlContainer();
…
}
2 声明一个 CAsyncSocket(或派生类)类的对象,作为主应用
程序类的类对象成员,例如:
class CMyDlg : public CDialog
{
…
private:
CAsyncSocket m_sMySocket;
…
};
3 在开始使用报路对象之前,必须先调用该对象的 Create 方
法,这时才真正创建了报路并为使用它作好了准备。如何调
用 Create 方法依据于该报路对象将被如何使用。如果希望把
报路用于连接到另一个应用程序,即客户机应用程序,则不
必向 Create 方法传递任何参数,例如:
…
if(m_sMySocket.Create())
// Continue on
else
// Perform error handling here
…
如果报路用于侦听与之连接的应用程序,即服务器应用程
序,则必须至少传递所侦听的报路的端口号,例如:
if(m_sMySocket.Create(4000))
// Continue on
else
// Perform error handling here
还可以在 Create 方法的调用中包含其他的参数,如要创建的
报路类型、报路应该响应的事件和报路应侦听的地址。
该函数的原型如下:
BOOL Create( UINT nSocketPort = 0,
int nSocketType = SOCK_STREAM,
long lEvent = FD_READ | FD_WRITE | FD_OOB |
FD_ACCEPT | FD_CONNECT | FD_CLOSE,
LPCTSTR lpszSocketAddress = NULL );
参数:
nSocketPort 指定所创建报路使用的端口,如果希望由
Windows Sockets 选择端口,该值为0。
nSocketType 指定所创建报路的类型:
SOCK_STREAM 提供基于全双工连接的、
可靠的、顺序字节流,使用TCP 协议。
SOCK_DGRAM 支持数据报(即无连接、不
可靠的、固定最大长度包) 使用UDP协议。
lEvent
指定所创建报路希望收到的网络事件,缺
省值表示希望接收所有网络事件。
lpszSocketAddress 指定所创建报路用于连接的网络地址,例
如:128.56.22.8。
返回值:
如果调用成功,返回非 0 错误代码;否则返回 0,并且可以
通过调用函数 GetLastError 检索一个指定的错误代码。用于
Create 的错误码如下:
WSANOTINITIALISED
A successful AfxSocketInit must occur
before using this API.
WSAENETDOWN
The Windows Sockets implementation
detected that the network subsystem
failed.
WSAEAFNOSUPPORT
The specified address family is not
supported.
WSAEINPROGRESS
A blocking Windows Sockets operation
is in progress.
WSAEMFILE
No more file descriptors are available.
WSAENOBUFS
No buffer space is available. The socket
cannot be created.
WSAEPROTONOSUPPORT The specified port is not supported.
WSAEPROTOTYPE
The specified port is the wrong type for
this socket.
WSAESOCKTNOSUPPORT The specified socket type is not
supported in this address.
13.1.3 建立连接
创建了报路之后,便可以准备用它建立一个连接。建立一个
连接包括三个步骤,其中两步在服务器(侦听连接的应用程
序)上进行,另一步则在客户机(申请连接的应用程序)上进
行。按照在建立连接中的先后顺序,这三个步骤描述如下:
1 启动侦听
服务器应用程序必须首先通过调用方法 Listen 来告诉报路去
侦听即将发生的连接。
Listen 方法的原型如下:
BOOL Listen( int nConnectionBacklog = 5 );
参数:
nConnectionBacklog
等待状态未决连接(等待完成连接)的最
大排队长度,有效值范围1 - 5,缺省值为 5。
返回值:
如果调用成功,返回非0;否则返回0,并且可以通过调用函
数GetLastError 检索指定的错误代码。Listen 的错误码如下:
WSANOTINITIALISED
A successful AfxSocketInit must occur
before using this API.
WSAENETDOWN
The Windows Sockets implementation
detected that the network subsystem
failed.
WSAEADDRINUSE
An attempt has been made to listen on an
address in use.
WSAEINPROGRESS
A blocking Windows Sockets operation is
in progress.
WSAEINVAL
The socket has not been bound with
Bind or is already connected.
WSAEISCONN
The socket is already connected.
WSAEMFILE
No more file descriptors are available.
WSAENOBUFS
No buffer space is available.
WSAENOTSOCK
The descriptor is not a socket.
WSAEOPNOTSUPP
The referenced socket is not of a type
that supports the Listen operation.
调用举例:
if( m_sMySocket.Listen())
// Continue on
else
// Perform error handling here
2 申请连接
申请建立一个连接是由客户机应用程序调用 Connect 方法来
完成的。Connect 方法的原型如下:
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
BOOL Connect( const SOCKADDR* lpSockAddr, int nSockAddrLen );
参数:
lpszHostAddress
被连接报路的网络地址:机器名,例如
“ftp.microsoft.com”, 或 “128.56.22.8”。
nHostPort
用于识别报路应用程序的端口号。
lpSockAddr
指向包含被连接报路地址的 SOCKADDR 结
构变量。
nSockAddrLen
在 SOCKADDR 结构变量中的被连接报路地
址的字节长度。
返回值:
如果调用成功,返回非0;否则返回0,并且可以通过调用函
数GetLastError 检索指定的错误码。Connect 的错误码如下:
WSANOTINITIALISED
A successful AfxSocketInit must occur
before using this API.
WSAENETDOWN
The Windows Sockets implementation
detected that the network subsystem
failed.
WSAEADDRINUSE
The specified address is already in use.
WSAEINPROGRESS
A blocking Windows Sockets call is in
progress.
WSAEADDRNOTAVAIL
The specified address is not available
from the local machine.
WSAEAFNOSUPPORT
Addresses in the specified family cannot
be used with this socket.
WSAECONNREFUSED
The attempt to connect was rejected.
WSAEDESTADDRREQ
A destination address is required.
WSAEFAULT
The nSockAddrLen argument is incorrect.
WSAEINVAL
Invalid host address.
WSAEISCONN
The socket is already connected.
WSAEMFILE
No more file descriptors are available.
WSAENETUNREACH
The network cannot be reached from this
host at this time.
WSAENOBUFS
No buffer space is available. The socket
cannot be connected.
WSAENOTSOCK
The descriptor is not a socket.
WSAETIMEDOUT
Attempt to connect timed out without
establishing a connection.
WSAEWOULDBLOCK
The socket is marked as nonblocking and
the connection cannot be completed
immediately.
客户机应用程序调用 Connect 方法必须传递两个参数:计算
机的名字或网络地址和应用程序要连往的端口。例如:
if(m_sMySocket.Connect(“thatcomputer.com”, 4000))
// Continue on
else
// Perform error handling here
或
if(m_sMySocket.Connect(“178.1.25.82”, 4000))
// Continue on
else
// Perform error handling here
3 接受连接
无论另一个应用程序何时打算连接到侦听应用程序,都会出
发一个事件,让侦听应用程序得知存在一个连接请求。侦听程
序必须通过调用 Accept 方法来接受连接请求。该方法需要使用
第二个 CAsyncSocket(或派生类)类对象,该对象用于连接请
求连接的应用程序。当一个报路处于侦听模式时,它将保持在
这种模式。一旦侦听报路接收到连接请求,它就会创建另一个
报路来连到请求连接的应用程序。这个报路不需要调用 Create
方法来建立,因为 Accept 方法会创建该报路。Accept 方法的原
型如下:
virtual BOOL Accept( CAsyncSocket& rConnectedSocket,
SOCKADDR* lpSockAddr = NULL,
int* lpSockAddrLen = NULL );
参数:
rConnectedSocket
识别一个新的对连接有效的报路的引用。
lpSockAddr
指向SOCKADDR 结构变量,用于接收请求
连接的报路地址。如果 lpSockAddr 和/或
lpSockAddrLen 等于 NULL,则没有被接受报
路的远程地址信息返回。
lpSockAddrLen
指向包含 SOCKADDR 结构变量中报路地址
的字节长度的变量,调用函数时,该变量
中包含的是由 lpSockAddr 所指的空间可能
长度,函数返回时,该变量中包含返回的
报路地址的实际字节长度。
调用 Accept 方法的方式如下:
if(m_sMySocket.Accept(m_sMySecondSocket))
// Continue on
else
// Perform error handling here
此时,申请连接的应用程序已经被连接到侦听应用程序的第
二个报路上(如,上例中的报路类对象 m_sMySecondSocket)。
13.1.4 发送和接收消息
通过连接的报路发送和接收消息有点复杂。由于可以用报路
发送和接收任何类型的数据,因此报路不关心数据的内容,发
送和接收数据的函数只期待得到一个指向某个通用缓冲区的指
针。当发送数据时,该缓冲区应该保存要发送的数据。当接收
数据时,该缓冲区将接收到的数据复制到其中。如果发送和接
收的数据是字符串和文本,就可以很简单地利用缓冲区在传递
的数据和 CString 类对象之间进行转换。通过报路连接消息可以
使用 Send 方法发送数据,该方法的原型如下:
virtual int Send( const void* lpBuf, int nBufLen, int nFlags = 0 );
参数:
lpBuf
指向包含被发送数据的缓冲区。
nBufLen
缓冲区中数据的长度(以字节为单位)。
nFlags
指定该函数的调用方式。该函数的语义取决于报
路的选项和参数 nFlags。该参数可以由下列值组
合构成:
MSG_DONTROUTE
指定数据不受路由支配。
Windows 报路提供者可以选择忽略此标记。
MSG_OOB
发送紧急数据(仅用于全双工连接
的字节流 SOCK_STREAM)。
返回值:
如果调用成功,返回被发送的数据的字节数;否则返回
SOCKET_ERROR。调用 GetLastError 可检索的错误标记如下:
WSANOTINITIALISED A successful AfxSocketInit must occur before
using this API.
WSAENETDOWN
The Windows Sockets implementation detected
that the network subsystem failed.
WSAEACCES
The requested address is a broadcast address,
but the appropriate flag was not set.
WSAEINPROGRESS
A blocking Windows Sockets operation is in
progress.
WSAEFAULT
The lpBuf argument is not in a valid part of the
user address space.
WSAENETRESET
The connection must be reset because the
Windows Sockets implementation dropped it.
WSAENOBUFS
The Windows Sockets implementation reports a
buffer deadlock.
WSAENOTCONN
The socket is not connected.
WSAENOTSOCK
The descriptor is not a socket.
WSAEOPNOTSUPP
MSG_OOB was specified, but the socket is not of
type SOCK_STREAM.
WSAESHUTDOWN
The socket has been shut down; it is not possible
to call Send on a socket after ShutDown has
been invoked with nHow set to 1 or 2.
WSAEWOULDBLOCK The socket is marked as nonblocking and the
requested operation would block.
WSAEMSGSIZE
The socket is of type SOCK_DGRAM, and the
datagram is larger than the maximum supported
by the Windows Sockets implementation.
WSAEINVAL
The socket has not been bound with Bind.
WSAECONNABORTED The virtual circuit was aborted due to timeout or
other failure.
WSAECONNRESET
The virtual circuit was reset by the remote side.
调用 Send 发送数据的典型代码如下:
CString strMyMessage;
int iLen;
int iAmtSent;
…
iLen = strMyMessage.GetLength();
iAmtSent = m_sMySocket.Send((LPCTSTR(strMyMessage), iLen);
if(iAmtSent == SOCKET_ERROR)
// Do some error handling here
else
// Everything’s fine
当得到的数据是从其他应用程序接收到的时候,在接收数据
的应用程序中,一个事件就会被触发。这将使应用程序知道它
可以接收并处理消息。为了得到消息,必须调用 Receive 方法。
Receive 方法的原型如下:
virtual int Receive( void* lpBuf, int nBufLen, int nFlags = 0 );
参数:
lpBuf
指向接收数据的缓冲区。
nBufLen
缓冲区中所接收数据的长度(以字节为单位)。
nFlags
指定该函数的调用方式。该函数的语义取决于报
路的选项和参数nFlags。该参数可以由下列值组
合构成:
MSG_PEEK
窥视发来的数据,将数据复制到缓
冲区中,但不从输入队列中删除。
MSG_OOB
处理紧急数据。
返回值:
如果调用成功,返回所接收的数据的字节数;如果连接已经
关闭,则返回 0;否则返回 SOCKET_ERROR。调用 GetLastError
可以检索的错误标记如下:
WSANOTINITIALISED A successful AfxSocketInit must occur before
using this API.
WSAENETDOWN
The Windows Sockets implementation detected
that the network subsystem failed.
WSAENOTCONN
The socket is not connected.
WSAEINPROGRESS
A blocking Windows Sockets operation is in
progress.
WSAENOTSOCK
The descriptor is not a socket.
WSAEOPNOTSUPP
MSG_OOB was specified, but the socket is not of
type SOCK_STREAM.
WSAESHUTDOWN
The socket has been shut down; it is not possible
to call Receive on a socket after ShutDown has
been invoked with nHow set to 0 or 2.
WSAEWOULDBLOCK The socket is marked as nonblocking and the
Receive operation would block.
WSAEMSGSIZE
The datagram was too large to fit into the
specified buffer and was truncated.
WSAEINVAL
The socket has not been bound with Bind.
WSAECONNABORTED The virtual circuit was aborted due to timeout or
other failure.
WSAECONNRESET
The virtual circuit was reset by the remote side.
调用 Receive 发送数据的典型代码如下:
char *pBuf = new char[1025];
int iBufSize = 1024;
int iRcvd;
CString strRecvd;
…
iRcvd = m_sMySocket.Receive(pBuf, iBufsize);
if(iRcvd == SOCKET_ERROR)
// Do some error handling here
else
{
pBuf[iRcvd] = NULL;
strRecvd = pBuf;
// Continue processing the messge
}
…
13.1.5 结束连接
当一个应用程序完成了与另一个应用程序之间的所有通信之
后,就可以调用 Close 方法来结束用于通信的报路连接。Close
方法的原型如下:
virtual void Close( );
调用方式如下:
…
m_sMySocket.Close();
…
Close 函数是 CAsyncSocket 类所提供的方法中少数几个没有
返回值的函数之一,因此,不可能像前面的所叙述的函数那
样,通过捕获返回值来判断是否发生错误。
13.1.6 报路事件
创建 CAsyncSocket 的自定义派生类的主要原因是希望能捕获
到在报路对象各种情况下所触发的事件。CAsyncSocket 类通过
提供一系列可以为这些事件所调用虚函数。这些函数都使用了
相同的定义代码结构(唯一的区别是函数名),它们存在的目
的就是为了在派生类中被重定义。所有这些函数都被声明为
CAsyncSocket 类的保护成员,因此,也应该被声明为派生类的
保护成员。所有这些函数都只有一个整型参数,该参数是一个
出错码,程序应该对该参数进行检测以确保没有错误发生。下
表列出了这些事件函数和及其代表的事件。
函数名
OnAccept
OnClose
OnConnect
OnReceive
OnSend
事件说明
通知侦听报路:能够接受一个请求调用 Accept 实现接受的
未决连接。
通知通信报路:与它连接的通信报路已经关闭。
通知正在进行连接操作的报路:完成了连接尝试,连接结果
可能成功或失败。
通知通信报路:通讯缓冲区中有需要调用 Receive 接收的数
据。
通知通信报路:现在能够调用 Send 发送数据。
13.1.7 检测错误
只要 CAsyncSocket 类的任何一个有返回值的函数返回了错误
信息(大多数函数返回 FALSE,而 Send 和 Receive 函数返回
SOCKET_ERROR),调用 GetLastError 方法获取相应的错误码。
根据错误码,你可以安排相应的恰当的显示信息和处理。检测
错误的典型代码如下:
…
int iErrCode;
iErrCode = m_sMySocket.GetLastError();
switch(iErrCode)
{
case WASNOTINITIALISED:
AfxMessageBox(“Windows socket has not be initialized.”);
break;
case WSAENETDOWN
AfxMessageBox(“The network subsystem failed.”);
break;
…
}
…
13.2 创建网络通讯应用程序
为了突出如何在应用程序中实现网络通信功能,而简化程序
中那些与网络通信无关的内容,本章的实例将创建一个简单的
对话框应用程序,它在 WinSock 连接中既可以充当客户机应用
程序也可以充当服务器应用程序。这将允许在报路连接的两端
各运行该应用程序的一个实例,它们可以是在同一台计算机
上,也可以在两台用网络连接的计算机上,从而可以清楚地了
解如何通过网络传递消息。一旦该应用程序的两个实例之间建
立了连接,每个程序实例就能够在它的对话框里键入一些文本
信息,并把这些信息发送到另一个程序实例。信息发送之后,
就被加入到已发送信息的列表中。接收到的每一条消息都将被
复制到已收到信息的列表中。
13.2.1 创建应用程序外壳
使用 AppWizard 创建一个名为 “Sock” 的 Dialog Based 应用程
序
项目。在创建过程中选择该应用程序具有 Windows Winsock 支持
在 Visual C++ 6.0 中选择该属性支持如下图所示:
在 Visual C++ .NET 中选择该属性支持如下图所示:
创建过程中的其他部分均接受默认选项。
13.2.2 编辑主对话框模板
删除对话框中原有的控件,加入下列控件:
·一组单选按钮控件:用于选择应用程序的运行模式:客户机
进程或服务器进程。
·一对文本编辑框控件:用于输入计算机名称和服务器要侦听
的端口地址。
·另一个文本编辑框控件:用于输入要发送到另一个进程的
文本信息。
·两个列表框控件:分别用于显示所有已发送和已收到的文本
信息。
·三个按钮控件: 分别用于 “侦听/连接”、“发送信息” 和
“关闭
各控件的类别和主要属性设置如下表所示:
控件类别
属性项目
属性值
Group Box
ID
Caption
IDC_STATICTYPE
Socket Type
Radio Button
ID
Caption
Group
IDC_RCLIENT
&Client
Checked
Radio Button
ID
Caption
IDC_RSERVER
&Server
Static Text
ID
Caption
IDC_STATICNAME
Server &Name:
Edit Box
ID
IDC_ESERVNAME
Static Text
ID
Caption
IDC_STATICPORT
Server &Port:
Edit Box
ID
IDC_ESERVPORT
Button
ID
Caption
IDC_BCONNECT
C&onnect
Button
Static Text
Edit Box
Button
Static Text
List Box
Static Text
List Box
ID
Caption
Disabled
ID
Caption
Disabled
ID
Disabled
ID
Caption
Disabled
ID
Caption
ID
Tab Stop
Sort
Selection
ID
Caption
ID
Tab Stop
Sort
Selection
IDC_BCLOSE
C&lose
Checked
IDC_STATICMSG
&Message:
Checked
IDC_EMSG
Checked
IDC_BSENDS
&end
Checked
IDC_STATIC
Sent:
IDC_LSENT
Unchecked
Unchecked
None
IDC_STATIC
Received:
IDC_LSENT
Unchecked
Unchecked
None
在主对话框类 CSockDlg 中,使用 ClassWizard 为对话框模板
中的一些控件增加关联变量。这些变量的关联控件、名称、类
型和用途如下表所示:
控件标识
变量名称
变量类型
用途说明
IDC_BCONNECT
m_ctrlConnect
CButton
请求/侦听连接操作的控件对象
IDC_EMSG
m_strMessage
CString
用户输入被发送消息的缓冲
IDC_ESERVNAME m_strName
CString
用户输入服务器名的缓冲
IDC_ESERVPORT
m_iPort
Int
用户输入报路端口的缓冲
IDC_LRECVD
m_ctrlRecvd
CListBox
显示被接收消息的控件对象
IDC_LSENT
m_ctrlSent
CListBox
显示被发送消息的控件对象
IDC_RCLIENT
m_iType
int
用户转换报路类型的缓冲
为了能使用 Connect 按钮既能把此报路程序置于客户应用程
序又可以置于服务器应用程序,需要使用 ClassWizard 为单选按
钮 IDC_RCLIENT 和 IDC_RSERVER 的 BN_CLICKED 消息添加一个
处理函数,使用户能通过选择这两个单选键实现报路类型的转
换。该函数命名为 OnRType,其定义代码如下:
void CSockDlg::OnRType()
{
// TODO: Add your control notification handler code here
// Sync the controls with variables
UpdateData(TRUE);
// Which mode are we in?
if(m_iType == 0)
// Set the appropriate text on the button
m_ctrlConnect.SetWindowText("C&onnect");
else
m_ctrlConnect.SetWindowText("&Listen");
}
13.2.3 创建 CAsyncSocket 类的派生类
为了能够捕获并响应报路事件,必须创建从 CAsyncSocket 类
继承的派生类 CMySocket,以便重新定义自己的事件函数新版
本,把事件传递给类对象所属的对话框类。在使用 ClassWizard
创建 CMySocket 后,为 CMySocket 和 CSockDlg 添加如下成员,
使主对话框能捕获并响应报路事件。
1 CMySocket 添加指向主对话框的指针
为了能把每个事件都传递给 CSockDlg 对象(在CMySocket 类
对象中调用 CSockDlg 对象的事件响应函数),应添加 CDialog 类
型的私有指针 m_pWnd ,用于指向 CSockDlg 对象。为了在运
行中动态设定指针值,还需要向 CMySocket 类中添加一个能设
置指针的公有成员函数 SetParent( CDialog* pWnd )。该函数的定
义代码如下:
void CMySocket::SetParent(CDialog *pWnd)
{ // Set the member pointer
m_pWnd = pWnd;
}
2 添加事件函数
报路类 CMySocket 中的事件函数将用于调用 CSockDlg 中的同
名成员函数完成对事件的响应。例如,要为 OnAccept 事件添加
响应,应在 CMySocket 中重载其基类 CAsyncSocket 的虚函数
OnAccept,定义新的事件函数版本。该函数的定义代码如下:
void CMySocket::OnAccept(int nErrorCode)
{ // TODO: Add your specialized code here and/or call the base class
// Were there any errors?
if(nErrorCode == 0)
// No, Call the dialog's OnAccept function
((CSockDlg*)m_pWnd)->OnAccept();
CAsyncSocket::OnAccept(nErrorCode);
}
由于函数中调用了 CSockDlg 的同名成员函数,所以需要将
CSockDlg 类的头文件包含在CMySocket 类的实现文件中。除了
OnAccept 外,还需要在CMySocket 类中重定义其他事件函数
OnConnect、OnClose 和 OnReceive,函数的定义与 OnAccept 的
定义类似。
3 在 CSockDlg 中增加报路类对象成员
在主对话框类 CSockDlg 中添加两个 CMySocket 类对象成员:
m_sConnectSocket
作为发送和接收消息的报路对象;
m_sListenSocket
作为侦听连接请求的报路对象。
这两个数据成员的访问权限均为私有。
4 初始化报路类对象成员
在 CSockDlg 类中添加了报路类对象之后,需要在该对话框类
的初始化函数 OnInitDialog 被调用时完成如下缺省设置:
·将报路类型选择设置为客户机应用程序 client。
·服务器名设置为 loopback(TCP/IP 网络协议中使用的特殊名
称,用以指明当前正在使用的计算机。它是计算机的内部名
称,它与 localhost 一样可解析为网络地址 127.0.0.1)。
如果需要连接应用程序是运行在同一台计算机上的,则上述
缺省设置是通常使用的。
·端口地址设置为 4000。
·把两个报路类对象中的对话框类指针 m_pWnd 设置为指向
CSockDlg 类对象。
被重定义的 OnInitDialog 代码如下:
BOOL CSockDlg::OnInitDialog()
{
CDialog::OnInitDialog();
…
// Initialize the control variables
m_iType = 0;
// Set socket as client
m_strName = "loopback";
// Set default name of server socket
m_iPort = 4000;
UpdateData(FALSE); // Update the controls
// Set the socket dialog pointers
m_sConnectSocket.SetParent(this);
m_sListenSocket.SetParent(this);
return TRUE; // return TRUE unless you set the focus to a control
}
13.2.4 连接两个应用程序
实现运行在报路两端的应用程序之间的连接是通过其中一端
应用程序按 Connect 按钮,向先于它进入侦听状态的另一端应
用程序发出通信连接请求,而另一端应用程序在侦听到连接请
求后,向请求端应用程序发出接受通知。如果上述操作都正确
无误,则连接成功。为此,应在 CSockDlg 类中添加 Connect 按
钮控件通知消息 BN_CLICKED 的映射和处理函数 OnBConnect 。
一旦单击 Connect / Listen 按钮后,不希望用户能够改变正在连
接操作的计算机的设置,改变应用程序的运行方式(服务器或
客户机),因此该函数应能禁止对话框中除用于信息通讯以外
的所有控件;同时根据应用程序的运行方式确定创建恰当的报
路和调用相应的事件函数。该函数的定义代码如下:
void CSockDlg::OnBconnect()
{ // TODO: Add your control notification handler code here
// Sync the variables with the controls
UpdateData(TRUE);
if(m_iType == 0)
// Are we running as client or server?
{
m_sConnectSocket.Create();
// Client, create a default socket
// Open the connection to the server
if(m_sConnectSocket.Connect(m_strName, m_iPort) == 0 &&
m_strName != "loopback" && m_strName != "localhost")
{
ErrorMessage(m_sConnectSocket.GetLastError());
m_sConnectSocket.Close();
return;
}
}
else
{
// Server, create a socket bound to the port specified
m_sListenSocket.Create(m_iPort);
if(m_sListenSocket.Listen() == 0) // Listen for connection requests
{
ErrorMessage(m_sListenSocket.GetLastError());
m_sListenSocket.Close();
return;
}
}
// Disable the connection and type controls
GetDlgItem (IDC_BCONNECT)->EnableWindow (FALSE);
GetDlgItem (IDC_ESERVNAME)->EnableWindow (FALSE);
GetDlgItem (IDC_ESERVPORT)->EnableWindow (FALSE);
GetDlgItem (IDC_STATICNAME)->EnableWindow (FALSE);
GetDlgItem (IDC_STATICPORT)->EnableWindow (FALSE);
GetDlgItem (IDC_RCLIENT)->EnableWindow (FALSE);
GetDlgItem (IDC_RSERVER)->EnableWindow (FALSE);
GetDlgItem (IDC_STATICTYPE)->EnableWindow (FALSE);
}
显示错误信息的成员函数定义如下:
void CSockDlg::ErrorMessage(int errorCode)
{
CString errorStr;
switch(errorCode)
{
case WSANOTINITIALISED:
errorStr =
_T("A successful AfxSocketInit must occur before using this API.");
break;
case WSAENETDOWN:
errorStr =
_T("The Windows Sockets implementation detected that the \
network subsystem failed.");
break;
case WSAEAFNOSUPPORT:
errorStr = _T("The specified address family is not supported.");
break;
case WSAEINPROGRESS:
errorStr = _T("A blocking Windows Sockets operation is in progress.");
break;
case WSAEMFILE:
errorStr = _T("No more file descriptors are available.");
break;
case WSAENOBUFS:
errorStr =
_T("No buffer space is available. The socket cannot be created.");
break;
case WSAEPROTONOSUPPORT:
errorStr = _T("The specified port is not supported.");
break;
case WSAEPROTOTYPE:
errorStr = _T("The specified port is the wrong type for this socket.");
break;
case WSAESOCKTNOSUPPORT:
errorStr =
_T("The specified socket type is not supported in this address.");
break;
case WSAEADDRINUSE:
errorStr =
_T("An attempt has been made to listen on an address in use.");
break;
case WSAEINVAL:
errorStr = _T("The socket has not been bound with Bind or is already \
connected.");
break;
case WSAEISCONN:
errorStr = _T("The socket is already connected.");
break;
case WSAENOTSOCK:
errorStr = _T("The descriptor is not a socket.");
break;
case WSAEOPNOTSUPP:
errorStr =_T("The referenced socket is not of a type that supports the \
Listen operation.");
break;
case WSAEADDRNOTAVAIL:
errorStr = _T("The specified address is not available from the local \
machine.");
break;
case WSAECONNREFUSED:
errorStr = _T("The attempt to connect was rejected.");
break;
case WSAEDESTADDRREQ:
errorStr = _T("A destination address is required.");
break;
case WSAEFAULT:
errorStr = _T("The nSockAddrLen argument is incorrect.");
break;
case WSAENETUNREACH:
errorStr =
_T("The network cannot be reached from this host at this time.");
break;
case WSAETIMEDOUT:
errorStr = _T("Attempt to connect timed out without establishing a \
connection.");
break;
case WSAEWOULDBLOCK:
errorStr = _T("The socket is marked as nonblocking and the
connection cannot be completed immediately.");
break;
case WSAEACCES:
errorStr =_T("The requested address is a broadcast address, but the \
appropriate flag was not set.");
break;
case WSAENETRESET:
errorStr = _T("The connection must be reset because the Windows \
Sockets implementation dropped it.");
break;
case WSAESHUTDOWN:
errorStr = _T("The socket has been shut down.");
break;
case WSAEMSGSIZE:
errorStr = _T("The datagram is larger than the maximum supported \
by the Windows Sockets implementation.");
break;
case WSAECONNABORTED:
errorStr = _T("The virtual circuit was aborted due to timeout or other \
failure.");
break;
case WSAECONNRESET:
errorStr = _T("The virtual circuit was reset by the remote side.");
break;
case WSAENOTCONN:
errorStr = _T("The socket is not connected.");
break;
}
AfxMessageBox(errorStr);
}
为了完成连接,还需要在CSockDlg 类中添加报路事件函数调
用的同名函数OnAccept、OnConnect。这些函数不需要传递任
何参数,也不返回任何结果。例如,对于OnAccept 函数(当一
个应用程序试图连接到被侦听到的报路时要调用此函数),需
要调用侦听报路对象的Accept 函数,并传递一个用于连接的通
讯报路类对象作为参数。如果应用程序接受连接,则可以启用
提示和编辑框,以便输入信息并把该信息发送到报路另一端的
应用程序。这些函数的定义代码分别如下所示:
void CSockDlg::OnAccept()
{
// Accept the Connection request
if(m_sListenSocket.Accept(m_sConnectSocket))
{
// Enable the text and message controls
GetDlgItem (IDC_EMSG)->EnableWindow (TRUE);
GetDlgItem (IDC_BSEND)->EnableWindow (TRUE);
GetDlgItem (IDC_STATICMSG)->EnableWindow (TRUE);
GetDlgItem (IDC_EMSG)->SetWindowText (_T(""));
m_ctrlSent.ResetContent();
m_ctrlRecvd.ResetContent();
}
}
void CSockDlg::OnConnect()
{
// Enable the text and message controls
GetDlgItem (IDC_EMSG)->EnableWindow (TRUE);
GetDlgItem (IDC_BSEND)->EnableWindow (TRUE);
GetDlgItem (IDC_STATICMSG)->EnableWindow (TRUE);
GetDlgItem (IDC_BCLOSE)->EnableWindow (TRUE);
GetDlgItem (IDC_EMSG)->SetWindowText (_T(""));
m_ctrlSent.ResetContent();
m_ctrlRecvd.ResetContent();
}
13.2.5 发送和接受消息
通讯两端的应用程序一旦能够建立连接,接下来的工作就是
添加能发送和接收信息的功能。如果两个应用程序间建立了连
接,用户就能够在对话框窗口中部的文本编辑框中输入文本信
息,然后单击 Send 按钮,把该文本信息发送到报路另一端的
应用程序。一旦文本信息成功发送,则将它被添加到已发送消
息的列表框中。要具备这种功能,需要在 CSockDlg 中添加 Send
按钮通知消息 BN_CLICKED 的映射和处理函数 OnBSend。该函
数的功能是:当按 Send 按钮时,应用程序检查是否有信息要
被发送。如果有信息被发送,则获取该信息的长度,发送该消
息。如果信息发送成功,则将此信息添加到已发送消息的列表
框中。函数的定义代码如下:
void CSockDlg::OnBsend()
{ // TODO: Add your control notification handler code here
int iLen;
int iSent;
// Sync the controls with the variables
UpdateData(TRUE);
// Is there a message to be sent?
if(m_strMessage != "")
{ // Get the length of the message
iLen = m_strMessage.GetLength();
// Send the message
iSent=m_sConnectSocket.Send(LPCTSTR(m_strMessage), 2*iLen);
// Were we able to send it?
if(iSent == SOCKET_ERROR)
ErrorMessage(m_sConnectSocket.GetLastErroe());
else
{
// Add the message to the list box.
m_ctrlSent.AddString(m_strMessage);
// Sync the variables with the controls
UpdateData(FALSE);
}
}
}
当报路对象的 OnReceive 事件函数被触发时,表明被传送信息
已经到达,可以用报路对象的 Receive 函数从报路中检索到该信
息。如果检索到了该信息,需要把它转换成 CString 类型数据,
并把它添加到接收消息的列表框中。上述操作将在 CSockDlg 类
的成员函数 OnReceive 中实现,该成员函数在报路类 CMySocket
的事件函数 OnReceive 中被调用。函数的定义代码如下:
void CSockDlg::OnReceive()
{
TCHAR Buf[1025];
int iBufSize = 1024;
int iRcvd;
CString strRecvd;
// Receive the message
iRcvd = m_sConnectSocket.Receive(Buf, iBufSize);
if(iRcvd == SOCKET_ERROR)
// Did we receive anything?
ErrorMessage(m_sConnectSocket.GetLastErroe());
else
{
Buf[ iRcvd/2 ] = 0;
// Truncate the end of the message
strRecvd = Buf;
// Copy the message to a CString
m_ctrlRecvd.AddString(strRecvd);
// Add the message to the received list box
UpdateData(FALSE);
// Sync the variables with the controls
}
}
13.2.6 终止连接
为了终止两个应用程序间的连接报路,客户应用程序的用户
可以通过按 Close 按钮来终止报路连接。在终止报路连接的操
作中:
·服务器应用程序接收到一个 OnClose 报路事件,关闭连接报
路的同时,禁止除已发送消息列表框和已接收消息列表框外
的所有其他的控件,继续侦听它需要侦听的(在连接配置中
设置的) 端口。
·在客户应用程序中,除 Close 和 Send 按钮以及输入被发送信
息的编辑框控件被禁止外,其他控件都处于允许状态,因为
客户程序可以改变某些信息,并打开与另一个服务器应用程
序的连接。
上述功能在 CSockDlg 类新添加的成员函数 OnClose 中实现,
具体的定义代码如下:
void CSockDlg::OnClose()
{
// Close the connected socket
m_sConnectSocket.Close();
// Disable the message sending controls
GetDlgItem (IDC_EMSG)->EnableWindow (FALSE);
GetDlgItem (IDC_BSEND)->EnableWindow (FALSE);
GetDlgItem (IDC_STATICMSG)->EnableWindow (FALSE);
GetDlgItem (IDC_BCLOSE)->EnableWindow (FALSE);
// Are we running in client mode
if(m_iType == 0)
{
// Yes, so anable the connection configuration controls
GetDlgItem (IDC_BCONNECT)->EnableWindow (TRUE);
GetDlgItem (IDC_ESERVNAME)->EnableWindow (TRUE);
GetDlgItem (IDC_ESERVPORT)->EnableWindow (TRUE);
GetDlgItem (IDC_STATICNAME)->EnableWindow (TRUE);
GetDlgItem (IDC_STATICPORT)->EnableWindow (TRUE);
GetDlgItem (IDC_RCLIENT)->EnableWindow (TRUE);
GetDlgItem (IDC_RSERVER)->EnableWindow (TRUE);
GetDlgItem (IDC_STATICTYPE)->EnableWindow (TRUE);
}
}
OnClose 不仅被报路类 CMySocket 相应的事件函数调用,而且还
需要响应按 Close 按钮发出的通知消息 BN_CLICKED 。因此,需
要该消息添加映射和处理函数 OnBClose,该函数的定义代码如
下:
void CSockDlg::OnBclose()
{
// TODO: Add your control notification handler code here
// Call the OnClose function
OnClose();
}
编译运行 “Sock” :
编译链接后,在同一台计算机上运行该应用程序的三个实
例。其中两个实例以服务器方式运行,它们的的区别仅在于所
侦听的端口不同,一个端口号为缺省值 4000,另一个为 3000;
另一个实例以客户机方式运行。测试的功能按顺序如下:
⑴ 两个服务器应用程序以不同的侦听端口号进入侦听状态;
⑵ 客户应用程序先以缺省计算机地址和端口号向服务器申请通
讯连接;
⑶ 从客户应用程序中发送信息;
⑷ 从接受了连接申请的服务器应用程序中发送信息;
⑸ 终止当前的通讯连接;
⑹ 在客户应用程序中改变申请的端口号为 3000 后,再次向服
务器发出通讯连接请求;
⑺ 从客户应用程序中发送信息;
⑻ 从接受了连接申请的服务器应用程序中发送信息;
⑼ 终止当前的通讯连接,并退出三个实例的运行。
MFC 提供的另一个 WinSock 类 CSocket 是 CAsyncSocket 的派
生类,该类继承了 CAsyncSocket 类的全部属性和方法,并通过
CSocketFile 和 CArchive 管理数据的发送和接收。CSocket 还提供
了阻塞功能,这对于 CArchive 的同步操作是十分重要的。具有
阻塞功能的函数包括 Receive、Send、ReceiveFrom、SendTo 和
Accept。这些函数都是从基类中继承的,但在如果调用被阻塞
时,必须等待相应的操作完成后才返回,而不像在基类中返回
WSAEWOULDBLOCK 错误码。如果这些函数调用正在被阻塞时
CSocket::CancelBlockingCall 被调用,则这些函数的调用将以返回
WSAEINTR 错误被终止。
WinSock API 函数是由系统动态链接库 Ws2_32.dll 提供的,因
此要在应用程序直接使用 WinSock API 函数(不使用 MFC 提供
的 WinSock 类),就必须在使用 WinSock API 函数之前通过调用
一个 SDK API 函数 WSAStartup 初始化动态链接库 Ws2_32.dll 的
使用;并在不再需要 WinSock API 函数支持时(例如程序退出之
前),调用另一个 SDK API 函数 WSACleanup 终止动态链接库
Ws2_32.dll 的使用。本章中的实例 “NetPing” 就是一个直接使用
WinSock API 函数进行网络中两台主机之间 ping 操作的程序。
13.3 因特网编程
随着计算机网络系统(硬件连接和软件平台)的不断发展和
完善,特别是基于计算机网络系统的 Internet 的迅速发展和普
及,网络已经可以将全世界的计算机(通过 Internet 和 Intranet)
连接在一起。运行在这样结构的计算机网络上的软件平台也应
是能将网络中所有计算机上的资源安全地连接在一起,并能方
便的使用和管理它们,就像在单一计算机上一样。Windows 平
台从 Windows 2000 开始已经逐步成为一个真正的计算机网络操
作平台(如 Windows XP),并随着网络结构的变化和发展不断
地发展、健全和完善。
显然,在这样的操作系统平台上编写能在网络系统中运行应
用程序时,如果只有 WinSock 接口和 MFC WinSock 类的支持是
远远不够的,将会使程序的编写变得非常困难。为此,Windows
平台提供了 CGI(Common Gateway Interface)接口、 ISAPI
(Internet Server Application Interface)接口和 WinInet 接口支持网
络编程,MFC 也提供了相应的类,使得程序编写更为结构化和
方便。
在这样的计算机网络系统中,应用程序的运行模式从传统的
C(Client)-S(Server) 模式变化为 B(Browser)-S(Server) 模式。 B-S 模
式大大方便了客户-服务器结构的网络应用程序的开发,它不仅
适用于 Internet 也适用于 Intranet。 典型的 B-S 模式如图所示:
WWW Server running
your ISAPI Server
extensions and filters
Add http, ftp, and
gopher support
using WinInet
Asynchronous
Moniker transfer
Information without
blocking
Internet
WWW Client running a
Web Browser, displaying
Active documents and
ActiveX controls
支持上述网络编程的重要 MFC 函数和类如下:
1 全局函数:
AfxParseURL
解析 URL 字串,返回服务器和它的组件的类型。在
CInternetSession::OpenURL 中被使用。
AfxGetInternetHandleType
确定句柄的类型。
2 ActiveX 控件类:
COleControl
该类从 CObject <- CWnd 派生,增加了嵌入链接功能。
3 活动文档类:
CDocObjectServer
该类从 CObject <- CCmdTarget 派生,实现文档服务器接
口描述。
CDocObjectServerItem
该类从 CObject<-CCmdTarget<-CDocItem<-COleServerItem
派生。一个 CDocObjectServer 对象可以包含多个
CDocObjectServerItem 对象。
4 异步 Moniker 类:
CAsyncMonikerFilter
该类从 CObject <- CFile <- CMonikerFile 派生,提供在
ActiveX 控件中使用异步 Moniker 的功能。
ActiveX 控件中的 IMoniker 接口可以异步访问任何一个数
据流,包括从 URL 异步装载文件。
CDataPathProperty
该类从 CAsyncMonikerFilter 派生,实现异步装载一个嵌入
链接控件的属性。
5 因特网服务器接口 ISAPI 类:
CHttpServer
该类无基类,提供扩展适应 ISAPI 的 Http 服
务器功能的方法。
CHttpServerContext 该类无基类,提供 CHttpServer 需要的工具
环境,用于处理从 Http 客户发送到 Http 服
务器的数据。
CHttpFilter
该类无基类,创建和管理超文本传输协议
过滤器对象。
CHttpFilterContext
该类无基类,提供 CHttpFilter 需要的工具,
用于处理通过超文本传输协议过滤器传送
的数据。
CHtmlStream
该类无基类,管理 HTML 内存文件。
6 WinInet 类:
CInternetSession
该类从 CObject 派生,创建和初始化一个或
几个并发的 Internet 会话层,如果需要,向
代理服务器描述服务器连接。
CInternetConnection 该类从 CObject 派生,管理服务器连接。
CFtpConnection
该类从 CInternetConnection 派生,管理与
FTP Internet 服务器的连接,并且允许操作
服务器上的文档目录和文件。
CGopherConnection 该类从 CInternetConnection 派生,管理与
Gopher Internet 服务器的连接。
CHttpConnection
该类从 CInternetConnection 派生,管理与
HTTP Internet 服务器的连接。
CInternetFile
该类从 CObject <- CFile <- CStdioFile 派生,
允许使用 Internet 协议访问远程系统的文件。
CGopherFile
该类从 CInternetFile 派生,提供在 Gopher 服
务器上查找和读文件的功能。
CHttpFile
该类从 CInternetFile 派生,提供在 HTTP 服
务器上请求和读文件的功能。
CFileFind
该类从 CObject 派生,执行本地文件查找。
CFtpFileFind
该类从 CFileFind 派生,提供 FTP 服务器的
文件查找。
CGopherFileFind
该类从 CFileFind 派生,提供 Gopher 服务器
的文件查找。
CGopherLocator
该类从 CObject 派生,从 Gopher 服务器获
取“定位”,“定位”的类型,并使“定
位”对
CGopherFileFind 对象有效。
CInternetExcption
该类从 CObject <- CExcption 派生,描述
Internet 操作中发生的异常情况。
使用上述 MFC 类编写 B-S 模式的网络应用程序,尤其 ISAPI
服务器扩展程序仍然是一件难度较大的工作。Visual C++ NET 在
上述编程支持的基础上,又做了相应的扩充和完善,全面支持
网络应用程序的开发,并提供了很多工具,使得网络应用程序
的开发变得相对简单和轻松。
本节将通过基于 IE 的 Web 浏览器的创建、利用 ATL Server 技
术创建 Web 应用程序、利用 ATL Server 技术创建 Web 服务和使
用 Web 服务的应用程序四个实例,介绍因特网应用程序的开发
技术。
13.3.1 创建 Web 浏览器
Web 浏览器主要是基于 Navigator Netscape 和 Microsoft Internet
Explorer 两种风格,但不管是哪一种风格的 Web 浏览器都必须
具备以下基本功能:
·输入: 输入需要浏览的 Internet 地址,并导航到相应的网页。
·返回:导航到当前网页的上一个被浏览过的网页。
·前进:导航到当前网页的下一个被浏览过的网页。
·主页:返回被浏览站点的主页。
·刷新:刷新显示当前所浏览的网页内容。
·停止:中断对当前所浏览网页的下载。
除上述基本功能外,可以根据设计需求,在你所创建的 Web
浏览器中增加任何所需要的功能和相应的界面。
13.3.1.1 CHtmlView 类
CHtmlView 视图类是用于开发 Web 浏览器的关键类。该类是
在 Visual C++ 6.0 中增加的新类,为编写具有因特网浏览功能的
应用程序提供了方便。在 Visual C++ NET 中仍然使用。
CHtmlView 视图类是从 CFormView 视图类派生的,即在基本视
图中支持使用对话框模板设计所需要的各类控件和控件与视图
的数据交换的基础上,增加了强大的 Web 浏览功能。在文档视
图结构的应用程序中,视图是实现程序功能实现的主要窗口界
面,因此,不难看出在典型的文档视图结构中使用 CHtmlView
类作为视图的基类是开发 Web 浏览器应用程序的基本思路。
CHtmlView 视图类的常用成员函数的功能如下表所示:
成员函数
功能说明
GoBack
导航到浏览历史列表中的前一项
GoForward
导航到浏览历史列表中的下一项
GoHome
导航到浏览器的因特网选项或属性对话框中指定的当前主页或开始页
GoSearch
导航到浏览器的因特网选项或属性对话框中指定的当前查找页
Navigate
导航到由 URL 指定的资源
Navigate2
导航到由 URL 指定的资源, 或由一个全路径指定的文件
Refresh
刷新(重载)浏览器当前刷新层中的 URL 或显示的文件
Refresh2
刷新(重载)浏览器指定刷新层中的 URL 或显示的文件
Stop
终止任何一次未完成的导航、下载和动态页元素(例如,背景声
或动画)
PutPropty
设置给定对象关联的属性值
GetPropty
检取给定对象关联属性的当前值
ExecWB
执行浏览器中的一条命令
13.3.1.2 创建 Web 浏览器实例
1 创建一个单文档视图结构的应用程序项目 “HtmlBrowser”。注
意
选择 CHtmlView 作为项目视图类 CHtmlBrowserView 的基类。
2 在创建的应用程序项目过程中,向导为 CHtmlBrowserView 类自
动添加了重载函数 OnInitUpdate(), 该函数用于实现程序启动
时对确省的 Web 页面的连接和浏览。代码如下:
void CHtmlBrowserView::OnInitialUpdate()
{
CHtmlView::OnInitialUpdate();
Navigate2(_T(“ http://www.msdn.microsoft.com/visualc/ "),
NULL, NULL);
}
显然,如果希望程序启动时连接和浏览指定站点的某个指定
网页,只需要修改调用 Navigate2 的第一个参数所提供的网页
地址字串即可。
3 添加一个对话框栏 IDD_DIALOGBAR 用于输入要导航浏览的网
页地址、导航到当前页、导航到前一页、导航到后一页和停
止对当前页导航。
其中的控件 ID 和用途如下:
控件ID
IDC_STATIC
控件类型
CStatic
控件用途
显示 “地址:” 标题
IDC_EDIT_ADDRESS CEdit
输入要连接和浏览的网页地址
IDC_BUTTON_GO
CButton
发出连接和浏览当前网页的命令消息
IDC_BUTTON_NEXT
CButton
发出连接到下一被浏览网页的命令消息
IDC_BUTTON_BACK CButton
发出连接到上一被浏览网页的命令消息
IDC_BUTTON_STOP CButton
发出中断当前网页连接浏览的命令消息
4 在主框架 CMainFrame 类中添加一个 CDialogBar 类的对象成员
m_MyDialogBar,并在 CMainFrame::OnCreate 中对话框栏的创建
和定位代码:
if(!m_MyDialogBar.Create(this, IDD_DIALOGBAR, CBRS_TOP |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_HIDE_INPLACE,
IDD_DIALOGBAR))
{
TRACE0("Failed to create dialog bar\n");
return -1;
// fail to create
}
…
m_MyDialogBar.EnableDocking(CBRS_ALIGN_TOP |
CBRS_ALIGN_BOTTOM);
DockControlBar(&m_MyDialogBar);
…
5 如果需要为 m_MyDialogBar 的对话框拦窗口进行初始化定位,
则可在主框架 CMainFrame 类中为窗口消息 WM_SIZE 添加消
息映射和响应函数,并在该响应函数中实现对对话框拦窗口
的初始定位操作。另外还需要添加一个布尔数据成员 m_bInit
用于指示对话框栏窗口位置是否已经初始化,该成员初始值
为 false。窗口消息 WM_SIZE 的响应函数 OnSize 定义如下:
void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
CFrameWnd::OnSize(nType, cx, cy);
if(!m_bInit && m_MyDialogBar.GetSafeHwnd() &&
m_wndToolBar.GetSafeHwnd())
{
m_bInit = TRUE;
CRect rect, rect1;
m_wndToolBar.GetWindowRect(&rect);
m_MyDialogBar.GetWindowRect(&rect1);
rect1 += CSize(rect.Width(), -rect.Height());
DockControlBar(&m_MyDialogBar,
AFX_IDW_DOCKBAR_TOP, &rect1);
}
}
6 在主框架 CMainFrame 类中添加一个成员函数 GetEditText 用于
获取在对话框栏的地址控件中输入的被连接和浏览的网页地
址字串。
void CMainFrame::GetEditText(CString& str)
{
CEdit* pAddress =
(CEdit*)m_MyDialogBar.GetDlgItem(IDC_EDIT_ADDRESS);
pAddress->GetWindowText(str);
}
7 在 HtmlBrowserView.cpp 中添加预编译指令 #include “MainFrm.h”,
以便在 CHtmlBrowserView 类中引用主框架中的对话框栏;并
在该视图类中为对话框栏中的“转到”、“前进”、“后退”
和“停
止”按钮通知消息添加消息映射和响应函数:
ON_BN_CLICKED(IDC_BUTTON_GO,
&CHtmlBrowserView::OnButtonGo)
ON_BN_CLICKED(IDC_BUTTON_NEXT,
&CHtmlBrowserView::OnButtonNext)
ON_BN_CLICKED(IDC_BUTTON_BACK,
&CHtmlBrowserView::OnButtonBack)
ON_BN_CLICKED(IDC_BUTTON_STOP,
void CHtmlBrowserView::OnButtonGo()
{
CString str;
((CMainFrame*)(AfxGetApp()->GetMainWnd()))->GetEditText(str);
Navigate2(str, NULL, NULL);
}
void CHtmlBrowserView::OnButtonNext()
{
GoForward(); }
void CHtmlBrowserView::OnButtonBack()
{
GoBack();
}
void CHtmlBrowserView::OnButtonStop()
{
Stop();
}
8 编译连接应用程序项目 “HtmlBrowser”,缺省网页
“http://www.msdn.microsoft.com/visualc/” 的连接和浏览如下:
13.3.2 开发 Web 应用程序
所谓 Web 应用程序就是编写能在 Web 客户端浏览器中被调
用的,而由 Web 服务器提供执行被调用功能的应用程序。显
然,这样的程序是在服务器上运行的,而调度其运行是通过客
户端浏览器执行相应的 URL 实现的。
在 Windows 平台上开发 Web 应用程序的技术方案很多,主要
有通过通用网关 CGI 的开发、使用因特网服务器接口 ISAPI 的开
发、借助服务脚本 ASP(Active Server Pages) 的开发和使用 ATL
Server 的开发四种。
开发简便,程序实现功能的能力强,功能的执行效率高是所
有应用程序开发追求的目标。通过下表中对上述四种开发方案
的比较,不难看出选择使用 ATL Server 是较优的方案。
方案
CGI
ISAPI
ASP
优点
缺点
1 简单易懂。
2 能生成动态 HTML。
1 实现功能的能力有限。
2 每次调用启动一个进程,
消耗资源大,访问客户多
易引起系统崩溃。
1 实现功能的能力强。
2 功能执行效率高。
3 能生成动态 HTML。
1 支持技术复杂,开发难度大。
2 静态与动态 HTML 之间缺少
分割,维护困难。
1 易于掌握,开发效率高。
2 运行稳定,易于维护。
3 能生成动态 HTML。
1 需要使用脚本处理引擎解释执
行,功能执行效率低。
2 无法实现并发功能。
1 实现功能的能力强。
2 功能执行效率高。
3 能生成动态 HTML。
ATL Server 4 开发工具完备,开发效率高。
5 静态与动态 HTML 分割清晰,易
于维护。
1 支持技术复杂。
13.3.2.1 ATL Server 结构
ATL Server 实际是利用 ISAPI 扩充建立的,其结构如下:
URL(.srf)
ISAPI 扩展 DLL
Web 应用程序 DLL
调度管理
处理 .srf 文件
缓存
程序功能
客
IIS
户
HTML
服务
1 SRF 文件
SRF 是服务器响应文件(Server Response File)的简称,用
来请求调用 Web 应用程序,调用方法是在浏览器地址栏中输
入“http://<服务器名>/<虚拟目录名>/<服务器响应文件名.srf>”。
SRF 是一个文本文件,它由两部分内容组成:
① HTML 脚本 —— 被浏览的 Web 网页的静态部分。
② 特殊标记 —— 被浏览的 Web 网页的动态部分。
实现 Web 网页动态部分的代码和全部程序性逻辑保存在
编译生成的 Web 应用程序 DLL 中。特殊标记说明了在网页的
何处调用 Web 应用程序的哪个功能,从而使网页的静态和动
态部分在文本内容上被清晰的分开,同时又在执行时被有机
的结合起来。
一个 Web 应用程序可以在多个 SRF 中被调用,一个 SRF
可以调用多个 Web 应用程序。
2 IIS 服务器
IIS 是因特网信息服务(Internet Information Services)的简
称,是网站的一种 Web 服务器。 Web 服务器是一个软件,
用于管理 Web 网页,并使这些页可以通过本地网(同一台计
算机上)和因特网(服务器和客户浏览器在不同的计算机
上)被客户浏览。从软件逻辑的角度上看,访问远程服务器
与访问本地服务器之间并没有什么区别,因为 Web 服务器与
客户之间的关系结构没有什么不同。
注意,除 IIS 之外,还有许多其他商用 Web 服务器,如
Apache 服务器和 Iplanet 服务器等。但目前只有 IIS 服务器支
持 ATL Server。
3 ISAPI 扩展 DLL
ISAPI 扩展是指一个可以接收客户请求并发送回应的 DLL 。在
上图所示的 ATL Server 结构中 ISAPI 扩展只有一个,在程序开
发过程中,ISAPI 扩展 DLL 是应用程序向导添加的,一般无须
修改。
ISAPI 扩展 DLL 负责缓存 ATL Server 应用程序,根据 SRF 文
件中的特殊标记加载对应的 ATL Server 应用程序的 DLL 来处
理这些标记;当响应完成时,将响应回传给 IIS。
IIS 向 ISAPI 扩展 DLL 传递客户端请求时,总是通过调用
HttpExtensionProc 将一个数据结构 EXTENSION_CONTROL_BLOCK
传递给 ISAPI 扩展 DLL,从这个结构中可以获取 HTTP 头信息
或调用函数,并可用于读写客户流。
4 ATL Server 应用程序 DLL
在 ATL Server 结构中 ATL Server 应用程序 DLL 可以有多个,
每个 DLL 包含用于解析 SRF 文件中特殊标记的 C++ 类,并在
运行时将这些标记替换为所需要的 HTML 代码。这些标记替
换操作是由请求处理类(Request Handler Class)完成的。该
类的基类(模板)是 CRequestHandlerT,它可以作为所有用户
请求处理类的基类。在 CRequestHandlerT 中实现了 COM 接口
IPplacementHandler,调用该接口的方法用于向 HTML 传递一个
流,该流将作为完成一个 HTTP 请求的 HTTP 响应被返回。用
户请求处理类在基类功能的基础上增加了应用程序所要实现
的功能(通过 IPplacementHandler 调用的方法)代码。
综上所述,可以认为 ATL Server 实质上是一个利用 SRF 文件
作为 HTML 创建模板(HTML Creator Template)的 ISAPI 扩展应
用程序,它接收来自客户端对 SRF 文件的请求,将这个请求交
给 ATL Server 应用程序 DLL 中的请求处理类进行标记替换后,
动态地产生回应客户端请求的 Web 页。
13.3.2.2 开发环境支持
1 操作系统的选择
Windows NT 4 或 Windows 2000 SP 2 以上版本,例如 Windows
XP Professional。
2 IIS 服务器的安装
必须安装 IIS web 服务器,版本应该选择 5.0 以上版本。
3 浏览器的选择
必须将浏览器升级到 Internet Explorer 5.0 或以上版本。
13.3.2.2 Web 应用程序实例
1 创建一个简单 ATL Server 项目,实现在客户端浏览器上填写
个人信息,在确认后发送到服务器,并请求处理;服务器收
到确认的个人信息后,将收到的信息作为响应发还给客户
端,并一定格式显示在浏览器中。项目名为 “MyATLServer”。
在项目的创建向导对话框 “ATL Server Project Wizard” 中,通
过
“Developer Support Options” 属性页选择使用 “Attributed code”
(特征码)支持 。缺省为不支持。
2 分析 ATL Server 项目的构成
一个 ATL Server 项目实际上是由两个项目组成:
① ATL Server 应用程序 DLL 项目
用于创建 ATL Server 应用程序 DLL,以便为客户的请求提
供所需要的服务。这些服务是通过一个用户请求处理类实
现的,该类的定义和实现代码框架由项目向导自动添加。
因此,为实现客户请求所需要添加的服务功能代码(用户
请求处理类的属性、成员函数等)和对相应的服务器响应
文件(.SRF )的修改都是在该项目中进行的。本例中的该
项目的命名为 MyATLServer,项目中的用户请求处理类被
缺省命名为 CMyATLServerHandler。
② ISAPI 扩充 DLL 项目
用于创建 Web 应用程序所需要的 Internet 服务器应用接口
扩展 DLL。由于这些接口扩展对于绝大多数 Web 应用程序
都是一样的,因此,由向导在此项目缺省创建的代码一般
不需要任何修改。
一旦项目成功编译链接,则所生成的两个 DLL 和一个 SRF 文
件就被复制到 Web 应用程序文件目录中(缺省情况下,该目
录名为:“C:\Inetpub\wwwroot\用户项目名\”)即部署已建立。
如果更新了 Web 应用程序的任何功能代码或修改 SRF 文件内
容,则需要从 “Build” 菜单中选择 “Rebuild Solution” 即可重
新建
立部署。
3 调试运行向导缺省创建的 Web 应用程序项目
与其他 Visual C++ 项目一样,只需在 “Debug” 菜单中选择
“Start Without Debugging”:
此时,默认的浏览器将显示如下的结果(由向导缺省创建的
Web 应用程序所提供的服务):
4 分析 Web 应用程序的运行机制
分析 ATL Server 项目(本例中MyATLServer )的相关代码,就
不难发现 SRF 文件是如何在浏览器中被执行,产生上述显示
结果的。
⑴ 分析 MyATLServer.h 文件的相关代码:
protected:
// Here is an example of how to use a replacement tag with the
// stencil processor
[ tag_name(name="Hello") ]
HTTP_CODE OnHello(void)
{
m_HttpResponse << "Hello World!";
return HTTP_SUCCESS;
}
其中:
① 方括号中的代码即 Visual C++ .NET的新增概念 —— 方法
级属性。所谓方法级属性是类型的文本解释,它能指导编
译器在二进制文件中插入或修改代码,实现 SRF 文件中的
动态内容。[tag_name(name=“Hello”)] 为方法级属性 tag_name
在 SRF 文件内的模板标记 {{Hello}} 与命令模板处理器调用
函数 OnHello 的执行结果之间建立的替代映射关系。
② m_HttpResponse 是 CRequestHandlerT 类的一个公共成员变
量,是 CHttpResponse 类(封装了写响应流的功能)的实
例。通过 m_HttpResponse 进行输出,并最终在客户浏览
器中显示所期望的结果。
③ HTTP_CODE 等价于 DWORD 类型, HTTP_SUCCESS 表示
调用成功,它们均在 atlserr.h 头文件中被定义。
⑵ 分析服务器响应文件 .SRF(本例中的 MyATLServer.srf )
<html>
{{// use MSDN's "ATL Server Response File Reference" to learn about
SRF files.}}
{{handler MyATLServer.dll/Default}}
<head>
</head>
<body>
This is a test: {{Hello}} <br>
</body>
</html>
① {{// use MSDN‘s …}} 为注释行,不被 Web 应用程序 DLL 处
理,也不会显示在最终生成的 HTML 代码中。
② {{handler MyATLServer.dll/Default}} 为处理程序的声明行。每
个 SRF 文件都可以声明一个处理程序来分析处理页面中
的动态部分。 handler 是标记的关键字,后紧跟一个 Web
应用程序 DLL 的名称(本例为 MyATLServer.dll)。该声明
行通常位于 SRF 文件的最上方,其作用是指示服务器,
如果要对 SRF 文件中动态部分的标记进行处理,必须首
先加载被声明的动态库。/Default进一步指出,Web 应用程
序使用缺省请求处理类(本例为 CMyATLServerHandler)来
处理标记。
③ {{Hello}} 为 “替换标记” 声明。处理程序在解析 SRF 文件
时,用特定函数的执行结果来替换 “替换标记”。
④ SRF 文件中允许使用简单控制结构,如 if/else 和 while等。
5 修改缺省的 Web 应用程序
为了满足 Web 应用程序的设计功能需求,依据前面对 Web
应用程序运行机制的分析,对 Web 应用程序进行如下修改:
① 将原来的 SRF 文件修改为
<html>
{{handler MyATLServer.dll/Default}}
<head>
</head>
<body>
{{if Inputed}}
<table width = 300 ID = "Table1">
<tr> 已收到您注册的信息如下:</tr>
<tr><td> 姓名= {{Name}} </td></tr>
<tr><td> 性别= {{Sex}} </td></tr>
<tr><td> 年龄= {{Age}} </td></tr>
<tr><td> 地址= {{Adr}} </td></tr>
</table>
{{else}}
<form action = "MyATLServer.srf">
<table>
<tr> 请输入下列信息:</tr>
<tr><td> 姓名:<input name="name" type="text\"></td></tr>
<tr><td> 性别:<input name="sex" type="text\"></td></tr>
<tr><td> 年龄:<input name="age" type="text\"></td></tr>
<tr><td> 地址:<input name="adr" type="text\"></td></tr>
</table>
<input type = "submit" value = "确定">
</form>
{{endif}}
</body>
</html>
② 为 CMyATLServerHandler 添加数据成员,保存接收数据。
…
private:
bool m_bInputed;
// 控制变量
CString m_szUser;
// 用户名
CString m_age;
// 年龄
CString m_sex;
// 性别
CString m_adr;
// 地址
…
③ 修改的 CMyATLServerHandler::ValidateAndExchange,以便从
请求流中获取数据。
HTTP_CODE ValidateAndExchange()
{
m_HttpResponse.SetContentType("text/html");
// 接收表单输入
const CHttpRequestParams& FormFields =
m_HttpRequest.GetFormVars();
// 获取表单的 input 域的输入
m_szUser = FormFields.Lookup("name");
m_sex = FormFields.Lookup("sex");
m_age = FormFields.Lookup("age");
m_adr = FormFields.Lookup("adr");
// 如果所有的窗体变量都为空,则显示用户信息输入界面
if(m_szUser=="" && m_sex == "" && m_age == "" && m_adr == "")
m_bInputed = false;
else
m_bInputed = true;
return HTTP_SUCCESS;
}
ValidateAndExchange 的另一种修改如下:
HTTP_CODE ValidateAndExchange()
{
m_HttpResponse.SetContentType("text/html");
// 接收表单输入
const CHttpRequestParams& FormFields =
m_HttpRequest.GetFormVars();
// 获取表单的 input 域的输入
CValidateContext valCtx;
m_HttpRequest.FormVars.Exchange("name",&m_szUser,&valCtx);
m_HttpRequest.FormVars.Exchange("sex", &m_sex, &valCtx);
m_HttpRequest.FormVars.Exchange("age", &m_age, &valCtx);
m_HttpRequest.FormVars.Exchange("adr", &m_adr, &valCtx);
// 如果所有的窗体变量都为空,则显示用户信息输入界面
if(m_szUser == "" && m_sex == "" && m_age == "" && m_adr == "")
m_bInputed = false;
else
m_bInputed = true;
return HTTP_SUCCESS;
}
④ 在 CMyATLServerHandler 类中添加成员函数,用于处理在客
户端的浏览器中对登录的个人信息进行确认提交;以及在
确认提交操作后,在浏览器中显示从客户端接收到的个人
登记信息。
·处理确认提交操作的响应函数:
[ tag_name(name = "Inputed")]
HTTP_CODE OnInputed(void)
{
if(m_bInputed)
return HTTP_SUCCESS;
else
return HTTP_S_FALSE;
}
·显示姓名信息操作的响应函数:
[ tag_name(name = "Name")]
HTTP_CODE OnName(void)
{
m_HttpResponse << (LPCTSTR)m_szUser;
return HTTP_SUCCESS;
}
·显示性别信息操作的响应函数:
[ tag_name(name = "Sex")]
HTTP_CODE OnSex(void)
{
m_HttpResponse << (LPCTSTR)m_sex;
return HTTP_SUCCESS;
}
·显示年龄信息操作的响应函数:
[ tag_name(name = "Age")]
HTTP_CODE OnAge(void)
{
m_HttpResponse << (LPCTSTR)m_age;
return HTTP_SUCCESS;
}
·显示地址信息操作的响应函数:
[ tag_name(name = "Adr")]
HTTP_CODE OnAdr(void)
{
m_HttpResponse << (LPCTSTR)m_adr;
return HTTP_SUCCESS;
}
6 调试和运行修改后的 Web 应用程序
每次对 Web 应用程序的修改后,都必须先执行 “Build” 菜单
中
的 “Rebuild Solution” 命令,以便进行ATL Server 结构的重新配
置,然后再执行 “Debug” 菜单中的“Start Without Debugging”
命
令,运行修改后的结果;否则运行结果将不会体现程序修改
的结果。
修改后的 Web 应用程序的运行结果分为个人信息登录提交的
输入界面和接收提交后的个人登录信息显示输出界面两部
分,分别显示如下:
·个人信息登录提交的输入界面:
·接收提交后回显已登录信息的输出界面:
13.3.3 Web 服务开发
随着 Internet 的迅速发展和应用的日益广泛,越来越需要开发
跨网络分布运行的应用程序的开发。这类应用程序的运行环境
和结构的最大的特点是:
·功能服务的请求者和提供者是可以跨网络分布在任何一台计
算机上的。当然包括分布在同一台计算机上,即功能服务的
本地请求和提供。
·运行在跨网络分布的不同计算机上的操作系统是不同的,发
出服务请求和提供服务功能的程序的编程语言也是不同的。
不难看出,满足上述要求的应用程序不再是单计算机开发环境
中软件的概念,而是一种更为广泛的服务,即 “软件是服务”。
如何方便地在以 Internet 为中心建立的网络中实现安全、高效的
服务是软件工作者今后必须关注的重要课题。
13.3.3.1 Web 服务概述
1 什么是 Web 服务
Web 服务是一种解决跨网络应用集成的开发模式。由于 Web
服务是依赖于 XML 和其他 Internet 标准进行服务的请求和响应
的,因此被广泛地接受,从而建立了应用程序跨网络交互的
结构基础,为实现“软件是服务”提供了技术保障。
Web 服务又是一种开发应用程序的机制。Web 服务所提供的
特定的服务功能既可以在内部被单个应用程序使用,也可以
通过公开,提供给处于网络中任何计算机上的任何数量的应
用程序使用。基于 XML 的消息处理是 Web 服务的基本数据通
讯方式,从而在最大程序上消除了使用不同的操作系统、编
程语言和组件模型所创建的系统之间的差异,为实现数据和
系统之间的交互性提供了一种可行的解决方案 。
Web 服务的提供者既可以独立的 Web 应用程序,也可以是一
个较大型 Web 应用程序中的一个组件。而 Web 服务的使用者
几乎可以是任何种类的应用程序,其他 Web 服务、 Web 应用
程序、Windows 应用程序和控制台应用程序,只要这些应用
程序能够向 Web 服务发送服务请求消息,并能接收和处理来
自于 Web 服务的响应消息(提供服务)。
2 Web 服务与 ActiveX 组件之间的区别
ActiveX 组件和 Web 服务虽然都可以为使用者提供特定的功能
服务,但在运行方式和使用方法上有很大不同。
·使用 ActiveX 组件:ActiveX 的功能代码运行在使用者的计算
机上,因此,必须首先将 ActiveX 组件安装在本机上并为其
注册,然后根据组件的类型库生成接口文件。在实际编程
中使用该接口文件来访问组件服务。目前使用 ActiveX 组件
提供服务的方式仍然是最广泛的。
·使用 Web 服务:Web 服务的功能代码可能运行在因特网上
由 URL 地址所指向的任何计算机(当然包括本地计算机)
上。因此,只要在所编写的程序中通过访问提供服务的
URL,得到如何请求服务的 XML 描述,并使用该描述生成
接口文件,然后通过这个接口文件访问所需要的服务。
3 Web 服务与 Web 应用程序之间的区别
虽然 Web 服务与 Web 应用程序都使用基于 ATL Server 的开发
方式,但在实现方法上是截然不同的。
·Web 服务是基于简单对象访问协议 SOAP。因此,Web 服务
创建向导会在所建立的项目代码中自动添加 SOAP 句柄属
性,使得 Visual Studio 可以使用 SOAP 的全部细节的代码。
·Web 应用程序使用方法级属性(tag_name)将方法标记为
一个能从 .SRF 文件中调用的方法;而 Web 服务使用方法级
属性(soap_method)将方法标记为一个能从外部资源中调
用的方法。例如,对于在客户浏览器上显示 “Hello World”
的
方法, Web 应用程序与 Web 服务实现调用的代码区别如下
[ tag_name(name = “Hello”)]
// Web 应用程序
HTTP_CODE OnHello(void)
{
m_HttpResponse << “Hello World !”;
return HTTP_SUCCESS;
}
// Web 服务
[soap_method]
HRESULT HelloWorld( /* [in] */ BSTR bstrInput,
/* [out, retval] */ BSTR* bstrOutput )
{
CComBSTR bstrOut( L“Hello World !” );
bstrOut += bstrInput;
bstrOut += L“ !”;
*bstrOutput = bstrOut.Detach();
return S_OK;
}
·Web 应用程序项目会产生一个.SRF 文件,通过该文件调用
编写到项目DLL 中的功能;而 Web 服务项目产生一个 .htm
文件,通过该文件向用户展示编写到项目DLL 中的 Web 服
务的调用方法。
·Web 服务项目还产生一个.DISCO 文件,该文件记载了提供
Web 服务的位置,以便协助用户查询有关 Web 服务所提供
的方法。
4 重要的相关名词含义
·XML (Extensible Markup Language)
XML 于1996 年由万维网协会(W3C)下属的 “XML工作组”
开
发成功。它是在 HTML 的基础上演化而来的,它是可移植
的、获得普遍支持的开放技术(非专利性技术)。与 HTML
只适合展示数据而不便于提取数据不同, XML 专注于对数
据本身的描述。使用 XML,文档作者可以描述任何类型的数
据,包括数学公式、软件配置指南、乐谱、菜谱、财务报
表等等。无论是人还是机器,都能顺利阅读 XML 文档。正
是因为 XML 的这些特点,它被作为 Web 服务的最底层的基
础,使用 XML 描述 SOAP 消息、描述服务等。简而言之,
Web 服务中的所有数据都由 XML 表示,所以 Web 服务又被
<?XML version = “1.0”?>
<!-- XML example: article.xml
-->
<!-- Article structured with XML.
-->
<article>
<title> Simple XML </title>
<date> August 6, 2003 </date>
<author>
<firstName> Su </firstName>
<lastName> Fari </lastName>
</author>
<summary> XML is pretty easy . </summary>
<content> In this chapter, we present a wide variety of examples
that use XML .
</content>
</article>
其中:
① <?XML version = “1.0”?> 为可选的 “XML 声明”行,表示
文档
是 XML 文档。参数 version 指定文档使用的 XML 的版本。
② 标记 <!-- 与 --> 之间的内容为 XML 注释,可出现文档的
任何位置。
③ XML 使用 “标记”(Tag)来标记数据。标记名必须封
闭在
一对尖括号(<>)中。标记必须成对使用,以便对字符
数据进行定界。用于开始 XML 数据的标记称为 “起始标
记”,例如文档中的 <article>,<title>,<date>,<author>,
<firstName>,<lastName>,<summary> 和 <content>;用于
结束 XML 数据的标记称为“结束标记” 与 “起始标记”
④ 独立的标记单元(即包含在起始标记和结束标记之间的
一切内容)称为 “元素”。一个 XML 文档总体上只由一
个
元素构成,即 “根元素”,其中包含了文档中所有其他
元
素。
·SOAP(Simple Object Access Protocal)简单对象访问协议
SOAP 是一个基于 XML 的简单协议,用于在 Web 上交换各
种类型的数据信息。SOAP 本身只是定义了如何用 XML 来
格式化请求和响应的协议,而不包含应用程序或传输语
义,因此具有高度的模块化和很强的可扩展性。遵照SOAP
协议构造的服务请求消息和服务响应消息( SOAP 消息)
通常使用 HTTP 协议(也可以使用任何其他传输协议,例
如 FTP、SMTP 等)进行传输,两个支持 SOAP 的应用程序
正是依靠这种传输的 SOAP 消息实现服务互通。
·UDDI(Universal Description, Discovery, and Integration)通用说
明、发现和集成
UDDI 规范定义一个发布和发现有关 Web 服务的标准方式,
即 Web 服务在 Internet 上的定位和注册。对于 Web 服务的开
发商,UDDI 提供了一种发布 Web 服务的最佳可选方式;而
对于 Web 服务的用户,UDDI 提供了一种索引服务。
·DISCO 动态发现
DISCO 按 UDDI 规范确定 Web 服务在 Internet 上的位置,为
用户发现 Web 服务提供帮助。 DISCO 文件的内容使用 XML
格式描述。
13.3.3.2 Web 服务的优势
基于 Web 服务建立的分布式应用系统与采用运行在 Windows
操作系统上的分布式组件对象模型 DCOM (Distributed Component
Object Model) 或采用运行在 UNIX 操作系统上的通用对象请求代
理体系结构 CORBA (Common Object Request Broker Architecture)
建立的分布式应用系统相比具有以下优势:
1 Web 服务的平台是独立的
在通信中,支持 Web 服务的协议是 HTTP 和 SOAP 。 HTTP 几
乎是所有操作系统和硬件平台都支持的 Internet 标准协议,而
SOAP 是由 HTTP 承载传送的;另外,SOAP 是基于 XML 描述
的,该协议本身没有规定硬件、操作系统或语言等方面的规
范。因此,基于 Web 服务建立的分布式网络对象服务系统不
以任何方式依赖操作系统,是完全与操作系统、对象模型和
编程语言无关的松耦合 Web 分布式应用系统。这是依赖 RPC
(Remote Procedure Call)协议的 DCOM 和依赖 IIOP(Internet
Inter ORB Protocol)协议的 CORBA 所无法比拟的。
2 Web 服务可通过防火墙工作
COM 组件在 Internet 上使用需要使用非标准端口进行通信。因
此,在当今病毒泛滥的信息系统中,为了网络安全,必须要
求为 COM 对象配置类似防火墙的设备,这就会阻止通过某些
端口输入输出的信息。 Web 服务使用 HTTP 作为传输协议,
并通过端口 80 进行通信,而该端口是每个重要的 Web 服务
器用来接收和满足数据请求的端口,因此防火墙不能阻塞该
端口,所以 Web 服务始终可以通过防火墙工作。
3 Web 服务易于编写
由于有了 .NET 这种先进的开发平台的支持,在 Visual Studio
ATL Server 向导的引导下,创建一个基本的、可以工作的 Web
服务是一件非常轻松的事情。
13.3.3.3 创建 Web 服务
一个完整的 Web 服务应用实例在实现逻辑上是包括创建 Web
服务和访问 Web 服务两个分离的部分。尽管开发人员既可以
是 Web 服务的创建者,同时又是 Web 服务的使用者,但开发
过程彼此之间显然是分离和独立的。当然,Web 服务必须先
创建,然后才能被访问。
本节将描述如何使用 Visual Studio .NET 提供的相应的项目向导
创建一个能提供华氏温度与摄氏温度之间的相互变换功能的
简单 Web 服务;以及创建一个能访问该 Web 服务的应用程
序。
1 创建 ATL Server Web 服务项目
⑴ 在“File”菜单中选择“New”,然后选择 “Project…”,打开
“New
Project” 对话框。在该对话框中创建 ATL Server Web Service
项目“TempConvert4”。
⑵ 单击 “OK” 启动 “ATL Server Project Wizard” 。
⑶ 接受项目缺省创建属性,单击 “Finish” 完成项目的创建。
在所创建的解决方案中包含 “处理程序” 和 “ISAPI扩展”
两
个单独的项目。通常只需对 “处理程序” 项目进行修改
2 分析 Web 服务项目的结构成员
与 Web 应用程序解决方案中对 “ISAPI 扩展” 项目的实现一
样, Web 服务解决方案中的 “ISAPI 扩展” 项目也是通过 IIS
服
务器以 ISAPI 扩展形式对客户提供服务。向导为该项目缺省
添加的代码已经满足了上述功能需求,一般无须进行修改和
添加。这里只对 “处理程序” 项目中的结构成员和相应的代
码加以分析,以便在恰当的位置修改和添加服务功能代码。
⑴ 动态发现文件(.disco)。该文件由项目向导产生,并最终
被发布到 Web 服务所在的目录下,客户端的开发人员通过
访问该文件得到站点提供的 Web 服务(在创建访问 Web 服
⑵ 服务描述文件(.htm)。该文件也是由项目向导产生,它
用 XML 格式描述了 Web 服务名称、支持服务的命名空间、
服务所包含的方法名称、方法的输入参数和类型以及方法
的输出参数和类型等,以便充分了解 Web 服务、确定
SOAP 消息所含的内容、正确执行 Web 服务。该文件的内
容实际上是向导生成的头文件(本例的 “TempConver4.h”)
内容的 XML 格式描述。
⑶ 请求处理类。该类定义和实现的详细代码均在相应的头文
件(本例的 “TempConver4.h”),开发人员的编码工作主要
在该文件中;而相应的源文件(本例的 “TempConver4.cpp”)
的代码比较抽象,一般无须进行修改。TempConver4.h 的缺
省内容如下:
// TempConvert4.h : Defines the ATL Server request handler class
#pragma once
namespace TempConvert4Service
{
// all struct, enum, and typedefs for your webservice should go inside
// the namespace
// ITempConvert4Service - web service interface declaration
[ uuid("7A6BEBF4-403F-4E86-94AF-A473F2B39A1F"), object ]
__interface ITempConvert4Service
{
// HelloWorld is a sample ATL Server web service method.
// It shows how to declare a web service method and
// its in-parameters and out-parameters
[id(1)] HRESULT HelloWorld([in] BSTR bstrInput,
[out, retval] BSTR *bstrOutput);
// TODO: Add additional web service methods here
};
// TempConvert4Service - web service implementation
[
request_handler(name="Default", sdl="GenTempConvert4WSDL"),
soap_handler( name="TempConvert4Service",
namespace="urn:TempConvert4Service",
protocol="soap“ )
]
class CTempConvert4Service : public ITempConvert4Service
{
public:
// This is a sample web service method that shows how to use the
// soap_method attribute to expose a method as a web method
[ soap_method ]
HRESULT HelloWorld(/*[in]*/ BSTR bstrInput,
/*[out, retval]*/ BSTR *bstrOutput)
{
CComBSTR bstrOut(L"Hello ");
bstrOut += bstrInput;
bstrOut += L"!";
*bstrOutput = bstrOut.Detach();
return S_OK;
}
// TODO: Add additional web service methods here
}; // class CTempConvert4Service
} // namespace TempConvert4Service
该文件是命名空间 TempConvert4Service 的声明,其中:
·接口声明。从源代码不难看出,Web 服务通过该接口声
明向外界公开服务的内容,其中包括了 Web 服务方法的
声明。缺省接口声明中只有方法 HelloWorld,对所提供的
Web 服务并无什么具体意义,但却可以为开发者在声明
中添加新方法提供了非常有用的 “示范”。
·request_handler 属性成员。该属性指定了 Web 服务通过
HTTP 处理客户端的 SOAP 请求。参数 name 用于指定请
求处理类的别名,如 name=“Default”,则请求处理类别名
使用后面的类名称作为实参值。参数 sdl 指明了得到服务
描述文件的形式(本例中sdl = “GenTempConvert4WSDL”)。
·soap_handler 属性成员。该属性提供了解析 SOAP 请求的
功能,并将请求派发给对应的方法进行处理。参数 name
指定 Web 服务名称的字符串;如果该参数未指定,则该
参数使用后面的类名称作为实参值。参数 namespace 指
定 XML 命名空间的字符串,用来惟一地标识服务、方
法、数据类型;如果该参数未指定,则该参数也使用后
面的类名称作为实参值。参数 protocol 指定 Web 服务可
通过何种协议进行访问,目前该参数只能选择 “soap”。
·Web 服务处理类的定义。该类是前面定义的接口类的派
生类,它包含的 soap_method 属性的应用,确定了 SOAP
请求和回应的处理方式。模仿缺省方法 HelloWorld ,添加
需要的新方法(在前面的接口声明中已经声明过)。
3 添加 Web 服务方法
在向导所创建的缺省 “处理程序” 项目中,为 Web 服务类
添加
方法是开发人员创建 Web 服务最主要的工作。本例中需要添
加将华氏温度转换为摄氏温度的方法 ConvertF2C 和将摄氏温
度转换为华氏温度的方法 ConvertC2F 。具体代码如下:
⑴ 修改接口声明的定义代码,加入要添加的方法的声明:
__interface ITempConvert4Service
{
[id(1)] HRESULT HelloWorld([in] BSTR bstrInput,
[out, retval] BSTR *bstrOutput);
// TODO: Add additional web service methods here
[id(1)] HRESULT ConvertF2C([in] double dFahrenheit,
[out, retval] double* pdCelsius);
[id(1)] HRESULT ConvertC2F([in] double dCelsius,
[out, retval] double* pdFahrenheit);
⑵ 修改 Web 服务类的定义代码,加入新方法的实现代码:
// TODO: Add additional web service methods here
[ soap_method ]
HRESULT ConvertF2C(double dFahrenheit, double* pdCelsius)
{
if(!pdCelsius) return E_INVALIDARG;
*pdCelsius = ((dFahrenheit - 32) * 5) / 9;
return S_OK;
}
[ soap_method ]
HRESULT ConvertC2F(double dCelsius, double* pdFahrenheit)
{
if(!pdFahrenheit) return E_INVALIDARG;
*pdFahrenheit = dCelsius * 9 / 5 + 32;
return S_OK;
}
⑶ 在服务描述文件 “TempConvert4.htm” 中添加所增加的服务
方
法 “ConvertF2C” 和 “ConvertC2F” 的描述(摹仿缺省服务方
法
“HelloWorld” 的描述方法)。
<div>
<p>The following operations are supported. For a formal definition, please review the
<a href="http://localhost/TempConvert4/TempConvert4.dll?Handler=GenTempConvert4WSDL" >
Service Description</a>.</p>
<ul>
<li>HelloWorld</li>
<li>ConvertF2C</li>
<li>ConvertC2F</li>
</ul>
</div>
4 编译 Web 服务项目
由于 Web 服务的运行既需要 “处理程序” 项目生成的结果,
也
需要 “ISAPI 扩展” 项目生成的 DLL,因此,首先需要在
“Build”
菜单中选择 “Build Solution”命令。该命令的执行结果将生成
两
个项目的所有执行文件,并将 Web 服务运行所需要的文件复
制到本地服务器上。显然,在每次对解决方案的修改后,都
需要在 “Build” 菜单中选择 “Rebuild Solution”命令(也可以只
执
行被修改项目的生成操作,例如 “Rebuild TempConvert4” )。
对解决方案的生成操作完成之后,需要在 “Debug” 菜单中选
5 部署 Web 服务
若要使所创建的 Web 服务能被他人使用,就必须将 Web 服务
部署到支持客户端可访问的 Web 服务器上。部署的方式有两
种:一种是手动将 Web 服务所需要的文件复制到目标服务器
上;另一种是为 Web 服务添加安装项目,以便生成安装程
序。添加安装项目的方法如下:
⑴ 在 “File” 菜单中选择 “Add” 的 “New Project…”。
⑵ 在弹出的 “Add New Project” 对话框的左边 “Project types”
中
选择 “Other Project Types” 的 “Setup and Deployment” 后,
在对
话框右边的 “Templates” 中选择 “Web Setup Project” 并为项
目
命名为 “TempConvert4WebSetup” 后,按 “OK” 按钮。
⑶ 在 “File System…” 编辑窗中选择 “Web Application Folder”
文件
夹;在 “Solution Explorer” 中右击 “TempConvert4WebSetup”
弹
出快捷菜单,选择 “Add” -> “Project Output…” 菜单命令。
⑷ 在弹出的 “Add Project Output Group” 对话框中选择
“Content
Files” 后单击 “OK” 按钮确定。
⑸ 在 “File System…” 编辑器中展开 “Web Application Folder”
文件
夹,并找到 “bin” 文件夹;右击 “bin” 文件夹,在弹出的
快
捷菜单中选择 “Add” -> “Project Output…” 菜单命令。
⑹ 在弹出的 “Add Project Output Group” 对话框中选择
“Primary
Output” 和 “Debug Symbols” 后单击 “OK” 按钮确定。
⑺ 再次右击 “bin” 文件夹,在弹出的快捷菜单中选择 “Add”
->
“Project Output…” 菜单命令;
⑻ 在弹出的 “Add Project Output Group” 对话框中,首先将项
目
更改为 “TempConvert4Isapi”,然后选择 “Primary Output” 和
“Debug Symbols” 后单击 “OK” 按钮确定。
⑼ 在 “Solution Explorer” 管理器中右击
“TempConvert4WebSetup”
项目弹出快捷菜单;在菜单中选择 “Build” 菜单命令。
上述操作的结果是:
在本地机的 “TempConvert4\TempConvert4WebSetup\Debug” 目录
中生成了命名为 “TempConvert4WebSetup” 的安装软件包和命
名为 “setup” 的 EXE 应用文件。在需要部署该 Web 服务的服
务器上运行 “setup” 就可以完成对 Web 服务装载部署或卸载。
13.3.3.4 访问 Web 服务
建立一个完整的 Web 应用实例还需要创建一个访问 Web 服务
的应用程序(可以是任何种类的应用程序)。在客户端该应用
程序向所需要的 Web 服务发出请求(即调用服务方法),形成
相应的网络通信,在服务器端运行 Web 服务方法的代码,并将
运行结果向客户端返回。本例的访问 Web 服务是创建一个使用
“TempConvert4Service” 提供的温度转换服务方法的 Windows 应
用
程序。
1 创建访问 Web 服务的应用程序
使用 MFC 应用程序向导创建一个基于对话框的 Windows 应用
程序,命名为 “WebClient”,应用程序项目的属性均接受缺省
设置。在所创建的项目中修改对话框模板如下:
模板中各个控件的类型、标识值、用途、关联变量名和类型
如下表所示:
控件类型
控件ID
变量名
变量类型
用途
CStatic
IDC_STATIC
标识“华氏转换摄氏”控件组
CStatic
IDC_STATIC
标识“华氏”温度输入
CStatic
IDC_STATIC
标识转换后的“摄氏”温度输出
CEdit
IDC_FAHRENHEIT
m_Fahrenheit
double
接收输入的“华氏”温度值
CEdit
IDC_CELSIUS
m_Celsius
double
显示转换后的“摄氏”温度值
CButton
IDC_BUTTON1
执行“华氏转换摄氏”方法
CStatic
IDC_STATIC
标识“摄氏转换华氏”控件组
CStatic
IDC_STATIC
标识“摄氏” 温度输入
CStatic
IDC_STATIC
标识转换后的“华氏”温度输出
CEdit
IDC_FAHRENHEIT1
m_Fahrenheit1
double
接收输入的“摄氏” 温度值
CEdit
IDC_CELSIUS1
m_Celsius1
double
显示转换后的“华氏”温度值
CButton
IDC_BUTTON2
执行“摄氏转换华氏”方法
CButton
IDC_BUTTON3
关闭对话框退出程序运行
2 添加 Web 服务
所谓 “Web 引用” 就是通过“服务描述文件” 生成 “请求
处理类”
的 “代理类”。该 “代理类” 的方法表示 Web 服务所公开
的实际
方法。实现 “代理类” 方法的代码是由两部分代码组成:
·打包和发送 SOAP 服务请求消息的代码;
·接收和解包任何返回的 SOAP 响应消息的代码。
当客户端应用程序创建了 “代理类” 的一个实例后,就能够
调
用 Web 服务的方法,像在本地调用自己建立的方法一样。
Visual Studio .NET 提供的 “添加 Web 引用” 大大简化了查询
可
以使用的 Web 服务、生成 “请求处理类” 的 “代理类” 和
⑴ 在 “Solution Explorer” 或 “Class View” 或 “Resource View”
中右
击项目名,弹出快捷菜单。选择 “Add Web Reference … ”
命
令。也可以通过主菜单 “Project” 中选择该命令。
⑵ 执行 “Add Web Reference ” 命令将弹出 “Add Web
Reference ”
对话框。在该对话框可以通过以下操作为应用程序引用
Web 服务。
① “URL” 地址。指定 Web 服务的 URL 和.disco 文件名。指
定的方法三种:
·在地址栏中键入 Web 服务的 URL 和.disco 文件名。
·从下拉列表中选择一个以前用过的 Web 服务。
·使用地址栏下面的 “Start Browsing for Web Services” 窗
口
提供的方法搜索一个 Web 服务,例如,使用方法
“web
services on the local machine” 搜索一个在本地机的服务器
上已经部署的 Web 服务。
② 在确定了 Web 服务的 URL 和.disco 文件名之后,就可以
单击按钮 “Go” 发现所需要的 Web 服务(也可以使用
Start Browsing for Web Services 发现 Web 服务)。例如:
③ 点击所显示的 Web 服务名(本例的 “TempConvert4”),
可以展示该 Web 服务所提供的服务方法。例如:
④ 单击左边的按钮 “Add Reference” 完成在应用程序中添
加
Web 服务的代理类模板(例如 CTempConvert4ServiceT)
和实例类(例如 CTempConvert4Service)的定义和全部实
现代码。在 “Solution Explorer” 管理器中,不难发现,
添
加 Web 引用操作会在项目中加入 4 个文件:
· Web 服务的代理类模板和实例类的定义和实现文件
(本例的 “TempConvert4.h”)。该文件包含了代理类
模
板和实例类的定义和实现代码。其中不同服务方法的
实现代码的大部分相同,并具有相同的结构;不同之
处表现在发送服务请求和接收服务响应的数据结构不
同,本例中各个方法所使用的数据结构分别为:
HelloWorld 使用__CTempConvert4Service_HelloWorld_struct
ConvertF2C 使用__CTempConvert4Service_ ConvertF2C _struct
ConvertC2F 使用__CTempConvert4Service_ ConvertC2F _struct
另一不同之处就是发送服务请求的消息描述参数不
HelloWorld 的发送服务请求调用是
SendRequest(_T("SOAPAction: \"#HelloWorld\"\r\n"));
ConvertF2C 的发送服务请求调用是
SendRequest(_T("SOAPAction: \"# ConvertF2C \"\r\n"));
ConvertC2F 的发送服务请求调用是
SendRequest(_T("SOAPAction: \"# ConvertC2F \"\r\n"));
· Web 服务发现文件(本例的 “TempConvert4.disco”)。
·结果发现映射文件(本例的 “result.discomap”)。
·服务描述文件(本例的 “TempConvert4.wsdl”)。
该文件中包含了各个服务方法的调用描述,开发人员
可以依据这些描述正确调用 Web 服务。
例如本例中对服务方法 ConvertF2C 的描述如下:
…
<wsdl:message name="ConvertF2CIn">
<wsdl:part name="dFahrenheit" type="s:double" />
</wsdl:message>
<wsdl:message name="ConvertF2COut">
<wsdl:part name="return" type="s:double" />
</wsdl:message>
…
<wsdl:operation name="ConvertF2C">
<wsdl:input message="s0:ConvertF2CIn" />
<wsdl:output message="s0:ConvertF2COut" />
</wsdl:operation>
…
<wsdl:operation name="ConvertF2C">
<soap:operation soapAction="#ConvertF2C" style="rpc" />
<wsdl:input>
<soap:body use="encoded" namespace=
"urn:TempConvert4Service" encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/" />
</wsdl:input>
<wsdl:output>
<soap:body use="encoded" namespace=
"urn:TempConvert4Service" encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/" />
</wsdl:output>
</wsdl:operation>
在一个应用程序中可以添加多个 Web 服务引用,即一个应用
程序可以使用多个 Web 服务。如果访问 Web 服务的应用程序
的设计编程是在非可视化环境中进行的,则添加 Web 服务引
用的操作是通过调用命令工具 sproxy.exe ( “微软本机 Web
服务代理生成器(Microsoft Native Web Service Proxy
Generator)”)实现的。该命令工具软件可以从服务器端的
服务描述文件(.xtm 文件)中生成服务代理文件。例如:
sproxy /wsdl http://localhost/TempConvert4/TempConvert4.dll?
Handler=GenTempConvert4WSDL /out:TempConvert4.h
3 修改客户应用程序的 WebClientDlg.cpp 文件
·添加预编译命令:#include “TempConvert4.h”。
·添加命令空间使用说明:
using namespace TempConvert4Service;。
·为了使应用程序能够使用 COM 组件,因此需要在应用程序
的初始化操作中设置 COM 属性。在本例中,需要在对话框
类的 OnInitDialog 成员函数中设置COM 属性如下:
BOOL CWebClientDlg::OnInitDialog()
{
CoInitialize(NULL);
CDialog::OnInitDialog();
…
}
4 在应用程序中添加调用 Web 服务方法的代码。在本例中,是
通过添加对话框中 3 个按钮的消息响应函数,在函数中调用
Web 服务方法的。
·“转换摄氏” 按钮消息响应函数:
void CWebClientDlg::OnBnClickedButton1()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
CTempConvert4Service Service;
double bstrReturn;
HRESULT hr = Service.ConvertF2C(m_Fahrenheit, &bstrReturn);
if(SUCCEEDED(hr))
{
m_Celsius = bstrReturn;
UpdateData(FALSE);
}
else
AfxMessageBox(_T("连接Web服务错误!"));
Service.Cleanup();
}
·“转换华氏” 按钮消息响应函数:
void CWebClientDlg::OnBnClickedButton2()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
CTempConvert4Service Service;
double bstrReturn;
HRESULT hr = Service.ConvertC2F (m_Celsius1, &bstrReturn);
if(SUCCEEDED(hr))
{
m_Fahrenheit1 = bstrReturn;
UpdateData(FALSE);
}
else
AfxMessageBox(_T("连接Web服务错误!"));
Service.Cleanup();
}
·“结束操作” 按钮消息响应函数:
void CWebClientDlg::OnBnClickedButton3()
{
// TODO: Add your control notification handler code here
CoUninitialize();
CDialog::OnCancel();
}
5 编译链接应用程序 “WebClient”。