微软的MFC把复杂的WinSock API函数封装到类里,这使得编写网络应用程序更容易。CAsyncSocket类逐个封装了WinSock API,为高级网络程序员 提供了更加有力而灵活的方法。这个类基于程序员了解网络通讯的假设,目的是为了在MFC中使用WinSock,程序员有责任处理诸如阻塞、字节顺序和在Unicode与MBCS 间转换字符的任务。为了给程序员提供更方便的接口以自动处理这些任务,MFC给出 了CSocket类,这个类是由CAsyncSocket类继承下来的,它提供了比CAsyncSocket更高层的WinSock API接口。Csocket类和CsocketFile类可以与Carchive类一起合作来管理发送和接收的数据,这使管理数据收发更加便利。CSocket对象提供阻塞模式,这对于Carchive的同步操作是至关重要的。阻塞函数(如Receive()、Send()、ReceiveFrom()、SendTo() 和Accept())直到操作完成后才返回控制权,因此如果需要低层控制和高效率,就使用CasyncSock类;如果需要方便,则可使用Csocket类。
一些网络应用程序(如网络电话、多媒体会议工具)对实时性要求非常强,要求能够直接应用WinSock发送和接收数据。为了充分利用MFC 的优势,首选方案应当是MFC中的CAsyncSocket类或CSocket类,这两个类完全封装了WinSock API,并提供更多的便利。本实例介绍应用这两个类的编程模型,并引出相关的成员函数与一些概念的解释。
CSocket类是由CAsyncSocket继承而来的,事实上,在MFC中CAsyncSocket 逐个封装了WinSock API,每个CAsyncSocket对象代表一个Windows Socket对象,使用CAsyncSocket 类要求程序员对网络编程较为熟悉。相比起来,CSocket类是CAsyncSocket的派生类, 继承了它封装的WinSock API。
CASyncSocket:异步方式。发送就返回结果,无阻塞现象
CSocket:同步方式。只有发送完成后才返回结果,有阻塞现象
一个CSocket对象代表了一个比CAsyncSocket对象更高层次的Windows Socket的抽象,CSocket类与CSocketFile类和CArchive类一起工作来发送和接收数据,因此使用它更加容易使用。CSocket对象提供阻塞模式,因为阻塞功 能对于CArchive的同步操作是至关重要的。在这里有必要对阻塞的概念作一解释: 一个socket可以处于"阻塞模式"或"非阻塞模式",当一个套接字处于阻塞模式(即同步操作)时,它的阻塞函数直到操作完成才会返回控制权,之所以称为阻塞是因为此套接字的阻塞函数在完成操作返回之前什么也不能做。如果一个socket处于非阻塞模式(即异步操作),则会被调用函数立即返回。在CAsyncSocket类中可以用GetLastError 成员函数查询最后的错误,如果错误是WSAEWOULDBLOCK则说明有阻塞,而CSocket绝不会返回WSAEWOULDBLOCK,因为它自己管理阻塞。微软建议尽量使用非阻塞模式,通过网络事件的发生而通知应用程序进行相应的处理。但在CSocket类中,为了利用CArchive 处理通讯中的许多问题和简化编程,它的一些成员函数总是具有阻塞性质的,这是因为CArchive类需要同步的操作。
在Win32环境下,如果要使用具有阻塞性质的套接字,应该放在独立的工作线程中处理,利用多线程的方法使阻塞不至于干扰其他线程,也不会把CPU时间浪费在阻塞上。多线程的方法既可以使程序员享受CSocket带来的简化编程的便利,也不会影响用户界面对用户的反应。
CAsyncSocket类编程模型
在一个MFC应用程序中,要想轻松处理多个网络协议,而又不牺牲灵活性时,可以考虑使用CAsyncSocket类,它的效率比CSocket 类要高。CAsyncSocket类针对字节流型套接字的编程模型简述如下:
1、构造一个CAsyncSocket对象,并用这个对象的Create成员函数产生一个Socket句柄。可以按如下两种方法构造:
CAsyncSocket sock; //使用默认参数产生一个字节流套接字 Sock.Create(); |
CAsyncSocket*pSocket=new CAsyncSocket; intnPort=27; pSocket->Create(nPort,SOCK-DGRAM); |
BOOL Create( UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, LPCTSTR lpszSocketAddress = NULL ); |
该函数的参数有:
1)端口,UINT类型。注意:如果是服务方,则使用一个众所周知的端口供服务方连接;如果是客户方,典型做法是接受默认参数,使套接字可以自主选择一个可用端口;
2)socket 类型,可以是SOCK-STREAM(默认值,字节流)或SOCK-DGRAM(数据报);
3)socket的地址,例如"ftp.gliet.edu.cn"或"202.193.64.33"。
2、如是客户方程序,用CAsyncSocket∷Connect()成员函数连接到服务方;如是服务方程序,用CAsyncSocket∷Listen()成员函数开始监听,一旦收到连接请求,则调用CAsyncSocket∷Accept()成员函数开始接收。注意:CAsyncSocket ∷Accept()成员函数要用一个新的并且是空的CAsyncSocket对象作为它的参数,这里所说 的"空的"指的是这个新对象还没有调用Create()成员函数。
3、调用其他的CAsyncSocket类的Receive()、ReceiveFrom()、Send()和SendTo()等成员函数进行数据通信。
4、通讯结束后,销毁CAsyncSocket对象。如果是在栈上产生的CAsyncSocket对象,则对象超出定义的范围时自动被析构;如果是在堆上产生,也就是用了new这个操作符,则必须使用delete操作符销毁CAsyncSocket 对象。
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对象的Read()、Write()等函数在客户与服务方传送数据。
7、通讯完毕后,销毁CArchive、CSocketFile和CSocket对象。
我通过几个采用 CSocket 类编写并基于 Client/Server (客户端 / 服务端)的网络聊天和传输文件的程序 ( 详见: 源代码参考 ) ,在调试这些程序的过程中,追踪深入至 CSocket 类核心源码 Sockcore.cpp , 对于CSocket 类的运行机制可谓是一览无遗,并且对于阻塞和非阻塞方式下的 socket 程序的编写也是稍有体会。
阅读本文请先注意:
这里的阻塞和非阻塞的概念仅适用于 Server 端 socket 程序。socket 意为套接字,它与 Socket 不同,请注意首字母的大小写。
客户端与服务端的通信简单来讲:服务端 socket 负责监听,应答,接收和发送消息,而客户端 socket 只是连接,应答,接收,发送消息。此外,如果你对于采用 CSocket 类编写 Client/Server 网络程序的原理不是很了解,请先查询一下( 详见:参考书籍和在线帮助 )。
在此之前,有必要先讲述一下: 网络传输服务提供者, ws2_32.dll , socket 事件 和 socket window 。
1、网络传输服务提供者(网络传输服务进程), Socket 事件, Socket Window
网络传输服务提供者 ( transport service provider )是以 DLL 的形式存在的,在 windows 操作系统启动时由服务进程 svchost.exe 加载。当 socket 被创建时,调用 API 函数 Socket (在 ws2_32.dll 中), Socket 函数会传递三个参数 : 地址族,套接字类型 ( 注 2 ) 和协议,这三个参数决定了是由哪一个类型的 网络传输服务提供者 来启动网络传输服务功能。所有的网络通信正是由网络传输服务提供者完成 , 这里将 网络传输服务提供者 称为 网络传输服务进程 更有助于理解,因为前文已提到 网络传输服务提供者是由 svchost.exe 服务进程所加载的。
当 Client 端 socket 与 Server 端 socket 相互通信时,两端均会触发 socket 事件。这里仅简要说明两个 socket 事件:
网络传输服务进程 将 socket 事件 保存至 socket 的事件队列中。此外, 网络传输服务进程 还会向 socket window 发送消息 WM_SOCKET_NOTIFY , 通知有 socket 事件 产生,见下文对 socket window 的详细说明。
调用 CSocket::Create 函数后,socket 被创建。 socket 创建过程中调用 CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead) 。该函数的作用是:
2、阻塞模式
阻塞模式下 Server 端与 Client 端之间的通信处于同步状态下。在 Server 端直接实例化 CSocket 类,调用 Create 方法创建 socket ,然后调用方法 Listen 开始侦听,最后用一个 while 循环阻塞调用 Accept 函数用于等待来自 Client 端的连接,如果这个 socket 在主线程(主程序)中运行,这将导致主线程的阻塞。因此,需要创建一个新的线程以运行 socket 服务。
调试跟踪至 CSocket::Accept 函数源码:
while(!Accept(...))
{
// The socket is marked as nonblocking and no connections are present to be accepted.
if (GetLastError() == WSAEWOULDBLOCK)
PumpMessage(FD_ACCEPT);
else
return FALSE;
}
它不断调用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 类)判断 Server 端 socket 的事件队列中是否存在正在引入的连接事件 - FD_ACCEPT (见 1 ),换句话说,就是判断是否有来自 Client 端 socket 的连接请求。
如果当前 Server 端 socket 的事件队列中存在正在引入的连接事件, Accept 返回一个非 0 值。否则, Accept 返回 0,此时调用 GetLastError 将返回错误代码 WSAEWOULDBLOCK ,表示队列中无任何连接请求。注意到在循环体内有一句代码:
PumpMessage(FD_ACCEPT);
PumpMessage 作为一个消息泵使得 socket window 中的消息能够维持在活动状态。实际跟踪进入 PumpMessage 中,发现这个消息泵与 Accept 函数的调用并不相关,它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重绘消息)处于活动状态,而绝大部分的 socket window 消息被阻塞,被阻塞的消息中含有 WM_SOCKET_NOTIFY。
很显然,如果没有来自 Client 端 socket 的连接请求, CSocket 就会不断调用 Accept 产生循环阻塞,直到有来自 Client 端 socket 的连接请求而解除阻塞。
阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功连接, Server 端与 Client 端彼此相互调用 Send 和 Receive 方法开始通信。
3、非阻塞模式
在非阻塞模式下 利用 socket 事件 的消息机制, Server 端与 Client 端之间的通信处于异步状态下。
通常需要从 CSocket 类派生一个新类,派生新类的目的是重载 socket 事件 的消息函数,然后在 socket 事件 的消息函数中添入合适的代码以完成 Client 端与 Server 端之间的通信,与阻塞模式相比,非阻塞模式无需创建一个新线程。
这里将讨论当 Server 端 socket 事件 - FD_ACCEPT 被触发后,该事件的处理函数 OnAccept 是如何进一步被触发的。其它事件的处理函数如 OnConnect, OnReceive 等的触发方式与此类似。
在 1 中已提到 Client/Server 端通信时, Server 端 socket 正在接收来自 Client 端 socket 连接请求,这将会触发 FD_ACCEPT 事件,同时 Server 端的 网络传输服务进程 向 Server 端的 socket window (CSocketWnd )发送事件通知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件产生 , CsocketWnd 在收到事件通知消息后,调用消息处理函数 OnSocketNotify:
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
{
CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam);
CSocket::ProcessAuxQueue();
return 0L ;
}
消息参数 wParam 是 socket 的句柄, lParam 是 socket 事件 。这里稍作解释一下,CSocketWnd 类是作为 CSocket 类的 友元类 ,这意味着它可以访问 CSocket 类中的保护和私有成员函数和变量, AuxQueueAdd 和 ProcessAuxQueue 是 CSocket 类的静态成员函数,如果你对友元不熟悉,请迅速找本有关 C++ 书看一下友元的使用方法吧!
ProcessAuxQueue 是实质处理 socket 事件的函数,在该函数中有这样一句代码:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE);
其实也就是由 socket 句柄得到发送事件通知消息的 socket 指针 pSocket:从 m_pmapSocketHandle 中查找(见 1 )!
最后, WSAGETSELECTEVENT(lParam) 会取出事件类型,在一个简单的 switch 语句中判断事件类型并调用事件处理函数。在这里,事件类型是 FD_ACCEPT ,当然就调用 pSocket->OnAccept !
结束语
Server 端 socket 处于阻塞调用模式下,它必须在一个新创建的线程中工作,防止主线程被阻塞。
当有多个 Client 端 socket 与 Server 端 socket 连接及通信时, Server 端采用阻塞模式就显得不适合了,应该采用非阻塞模式 , 利用 socket 事件 的消息机制来接受多个 Client 端 socket 的连接请求并进行通信。
在非阻塞模式下,利用 CSocketWnd 作为所有 sockets 的消息池,是实现 socket 事件 的消息机制的关键技术。文中存在用词不妥和可能存在的技术问题,请大家原谅,也请批评指正,谢谢!
注: