一、引言
Internet进行网际间通讯,在WWW浏览、FTP、Gopher这些常规服务,以及在网络电话、多媒体会议等这些对实时性要求严格的应用中成为研究的热点,而且已经是必需的了。Windows环境下进行通讯程序设计的最基本方法是应用WindowsSockets实现进程间的通讯,为此微软提供了大量基于WindowsSockets的通讯API,如WinSockAPI、WinInetAPI和ISAPI,并一直致力于开发更快、更容易的通讯API,将其和MFC集成在一起以使通讯编程越来越容易。
MFC是VC编程环境最重要的组成部分,它为用户提供了一大批预先定义的类和成员函数,封装了大量的WindowsAPI。同时VC环境提供了与MFC对象和代码一起工作的专用工具:AppStudio源程序编辑器、AppWizard和ClassWizard。应用MFC,可以使Windows程序员用较少的时间和精力开发出复杂的通讯应用程序。
本文根据笔者自己在开发实时网络音频工具FreeTalk过程中的一些经验,介绍Windows环境下的常用API和封装它们的MFC类,重点介绍使用MFC的CAsyncsocket和CSocket类编写网络通讯程序的方法,这两个类封装了WinSockAPI,并使他们更容易使用和更适应于MFC编程环境。
二、Windows环境下的通讯API和相应的MFC类
1.WindowsSockets(WinSock)API
WindowsSockets定义了Windows的网络编程接口,它基于加利福尼亚大学伯克利分校的伯克利UnixSockets。WindowsSockets既包括BSD风格的例程,还加入了Windows的扩展部分,例如用于消息驱动的扩展函数。WindowsSockets可以运行在许多网络协议之上,包括TCP/IP、XNS、DECNet、IPX/SPX等。在Win32环境下,WindowsSockets提供线程安全。通过微软与标准组织的努力,为WinSock定义了应用程序设计接口(WinSockAPI),可以非常方便地利用下层的网络协议(如TCP/IP)进行网络通讯。
通过提供两个类CAsyncSocket和CSocket,MFC支持使用WinSockAPI通讯程序设计。MFC把复杂的WinSockAPI封装到类里,这使得编写应用程序更容易。CAsyncSocket类逐个封装了WinSockAPI,为高级网络程序员提供了更加有力而灵活的方法。这个类基于程序员了解网络通讯的假设,目的是为了在MFC中使用WinSock,程序员有责任处理诸如阻塞、字节顺序和在Unicode与MBCS间转换字符的任务。为了给程序员提供更方便的接口以自动处理这些任务,MFC给出了CSocket类,这个类是由CAsyncSocket类继承下来的,它提供了比CAsyncSocket更高层的WinSockAPI接口。Csocket类和CsocketFile类与Carchive类一起合作来管理发送和接收的数据,这使管理数据收发更加便利。CSocket对象提供阻塞模式,这对于Carchive的同步操作是至关重要的。阻塞函数[比如Receive()、Send()、ReceiveFrom()、SendTo()和Accept()]直到操作完成后才返回控制权,因此如果需要低层控制和高效率,就使用CasyncSock类;如果需要方便,则可使用Csocket类。2.Win32Internet(WinInet)API
微软公布了一些使Internet应用程序的设计比以前更快、更容易的API:WinInetAPI,它提供了中高层通信函数,这使访问主要的Internet协议变得相当容易。这些函数在程序员和WinSock驱动之间提供了隔离层。有4类WinInetAPI函数:通用WinInet函数、WinInet文件传输协议(FTP)函数、WinInetGopher函数、WinInet超文本传输协议(HTTP)函数。
事实上,MFC把WinInetAPI和ActiveX技术封装进类,使Internet编程更加容易,这些类包括CInternetSession、CInternetConnection、CInternetFile、CHttpConnection、CHttpFile、CGopherFile、CFtpConnection、CGopherConnection、CFileFind、CFtpFileFind、CGopherFileFind、CGopherLocator和CInternetException。
3.Internet服务器API(ISAPI)
微软的IIS是惟一与WindowsNTServer操作系统紧密集成的WWW服务器,它作为Internet/Intranet服务器应用范围很广。IIS允许扩展功能,这是通过ISAPI来实现的,ISAPI描述了与Internet服务器之间的接口。用ISAPI提供的工具,可建立高性能、高效率、满足商业安全及符合新的IIS标准的Internet服务器。同样,ISAPI在MFC中由典型的类所封装,包括CHttpFilter、CHttpFilterContext、CHttpServer、CHttpServerContext、RelatedClasses和CHtmlStream。
三、WinSockAPI的MFC封装类
一些网络应用程序(如网络电话、多媒体会议工具)实时性要求非常强,要求能够直接应用WinSock发送和接收数据。这时设计者应该选择直接应用WinSockAPI或者由MFC封装的WinSockAPI。新开发的应用程序中,为了充分利用MFC的优势,首选方案应当是MFC中的CAsyncSocket类和CSocket类,这两个类完全封装了WinSockAPI,并提供更多的便利。本文介绍应用这两个类的编程模型,并引出相关的成员函数与一些概念的解释。
1.CAsyncSocket类和CSocket类简述
CAsyncSocket类和CSocket类的继承关系由附图给出。CSocket类是由CAsyncSocket继承而来的,事实上,在MFC中CAsyncSocket逐个封装了WinSockAPI,每个CAsyncSocket对象代表一个WindowsSocket,使用CAsyncSocket类要求程序员对网络编程较为熟悉。相比起来,CSocket类是CAsyncSocket的派生类,继承了它封装的WinSockAPI。一个CSocket对象代表了一个比CAsyncSocket对象更高层次的WindowsSocket抽象,CSocket类与CSocketFile类和CArchive类一起工作来发送和接收数据,因此使用它更加容易。CSocket对象提供阻塞模式,因为阻塞功能对于CArchive的同步操作是至关重要的。在这里有必要对阻塞的概念作一解释:一个socket可以处于“阻塞模式”或“非阻塞模式”,当一个套接字处于阻塞模式(即同步操作)时,它的阻塞函数直到操作完成才会返回控制权,之所以称为阻塞是因为此套接字的阻塞函数在完成操作返回之前什么也不能做。如果一个socket处于非阻塞模式(即异步操作),则会被调用函数立即返回。在CAsyncSocket类中可以用GetLastError成员函数查询最后的错误,如果错误是WSAEWOULDBLOCK则说明有阻塞,而CSocket绝不会返回WSAEWOULDBLOCK,因为它自己管理阻塞。微软建议尽量使用非阻塞模式,通过网络事件的发生而通知应用程序进行相应的处理。但在CSocket类中,为了利用CArchive处理通讯中的许多问题和简化编程,它的一些成员函数总是具有阻塞性质的,这是因为CArchive类需要同步的操作。在Win32环境下,如果要使用具有阻塞性质的套接字,应该放在独立的工人线程中处理,利用多线程的方法使阻塞不至于干扰其他线程,也不会把CPU时间浪费在阻塞上。多线程的方法既可以使程序员享受CSocket带来的简化编程的便利,也不会影响用户界面对用户的反应。
2.CAsyncsocket类编程模型
在一个MFC应用程序中,要想轻松处理多个网络协议,而又不牺牲灵活性时,可以考虑使用CAsyncSocket类,它的效率比CSocket类要高。CAsyncSocket类针对字节流型套接字的编程模型简述如下:
(1)构造一个CAsyncSocket对象,并用这个对象的Create成员函数产生一个Socket句柄。可以按如下两种方法构造:
CAsyncSocketsock;
Sock.Create();
file://使用默认参数产生一个字节流套接字
或
CAsyncSocket*pSocket=newCAsyncSocket;
intnPort=27;
pSocket->Create(nPort,SOCK-DGRAM);
file://指定端口号产生一个数据报套接字
第一种方法在栈上产生一个CAsyncSocket对象,而第二种方法在堆上产生CAsyncSocket对象。第一种Create成员函数用缺省参数产生一个字节流套接字,第二种Create成员函数用指定的端口和地址产生一个数字报套接字。Create的参数有:
①端口,UINT类型。注意:如果是服务方,则使用一个众所周知的端口供服务方连接;如果是客户方,典型做法是接受默认参数,使套接字可以自主选择一个可用端口;
②socket类型。SOCK-STREAM(默认值)或SOCK-DGRAM;
③socket地址。例如“ftp.gliet.edu.cn”或“202.193.64.33”。
(2)如是客户方程序,用CAsyncSocket∷Connect成员函数连接到服务方;如是服务方程序,用CAsyncSocket∷Listen成员函数开始监听,一旦收到连接请求,则调用CAsyncSocket∷Accept成员函数开始接收。注意:CAsyncSocket∷Accept成员函数要用一个新的并且是空的CSocket对象作为它的参数,这里所说的“空的”指的是这个新对象还没有调用Create成员函数。
(3)调用其他的CAsyncSocket类成员函数进行通讯管理。
(4)通讯结束后,销毁CAsyncSocket对象。如果是在栈上产生的CAsyncSocket对象,则对象超出定义的范围时自动被析构;如果是在堆上产生,也就是用了new这个操作符,则必须使用delete操作符销毁CAsyncSocket对象。
3.CSocket类编程模型
使用CSocket对象涉及CArchive和CSocketFile类对象。以下介绍的针对字节流型套接字的操作步骤中,只有第3步对于客户方和服务方操作是不同的,其他步骤都相同。
(1)构造一个CSocket对象。
(2)使用这个对象的Create成员函数产生一个socket句柄。在客户方程序中,除非需要数据报套接字,Create一般情况下应该使用默认参数。而对于服务方程序,必须在调用Create时指定一个端口。注意:CArchive不能与数据报(UDP)套接字一起工作,因此对于数据报套接字,CAsyncSocket和CSocket的使用方法是一样的。
(3)如果是客户方套接字,则调用CAsyncSocket∷Connect与服务方套接字连接;如果是服务方套接字,则调用CAsyncSocket∷Listen开始监听来自客户方的连接请求,收到连接请求后,调用CAsyncSocket∷Accept接受请求,建立连接。注意:Accept成员函数需要一个新的并且为空的CSocket对象作为它的参数,解释同上。
(4)产生一个CSocketFile对象,并把它与CSocket对象关联起来。
(5)为接收和发送数据各产生一个CArchive对象,把它们与CSocketFile对象关联起来。切记CArchive是不能和数据报套接字一起工作的。
(6)使用CArchive对象在客户与服务方传送数据。(7)通讯完毕后,销毁CArchive、CSocketFile和CSocket对象。