2.08k likes | 2.2k Views
第十三章 网络通信与因特网应用程序. 由于 因特网 的的出现和迅速盛行,基于 网络通信 ,越来越多 的应用程序的运行环境不再是 同地 的 单机 系统,而是通过网络 (包括局域网和因特网)连接起来的 异地 的 多机 系统。从 Windows NT 和 Windows 95 开始,微软把 网络功能 逐步地融入到 其操作系统之中,使得 网络功能 在各类应用程序中,根据功能 需要完成不同程度和不同需要的网络任务已不是新鲜话题。例 如,对一个 Web 站点中运行的应用程序进行核实,判断是否已 经更新,并提示用户是否更新程序版本;又如,网络游戏程序
E N D
由于因特网的的出现和迅速盛行,基于网络通信,越来越多由于因特网的的出现和迅速盛行,基于网络通信,越来越多 的应用程序的运行环境不再是同地的单机系统,而是通过网络 (包括局域网和因特网)连接起来的异地的多机系统。从 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指定该函数的调用方式。该函数的语义取决于报 路的选项和参数 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. WSAEOPNOTSUPPMSG_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.
WSAEOPNOTSUPPMSG_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 类的保护成员,因此,也应该被声明为派生类的 保护成员。所有这些函数都只有一个整型参数,该参数是一个 出错码,程序应该对该参数进行检测以确保没有错误发生。下 表列出了这些事件函数和及其代表的事件。
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 中选择该属性支持如下图所示: 创建过程中的其他部分均接受默认选项。