在这一章的前面数个小节先简单介绍了一下Internet的基本知识,我想这些不需要太多去描述和学习了。
下面该书讨论CSocket, CAsyncSocket,并给出了一个小小的两人网络战场游戏,通过Socket相互连接。这里对Socket说明的实在甚少,让我自己去看看文档,深入MFC源代码去看看吧。
下面给出CSocket的类简化定义,此定义来自afxsock.h:
class CSocket : public CAsyncSocket { // 该定义中的成员变量、一些保护函数等被简化掉了 CSocket(); virtual ~CSocket(); BOOL Create(***); BOOL IsBlocking(); static CSocket* PASCAL FromHandle(SOCKET hSocket); BOOL Attach(SOCKET hSocket);
virtual BOOL Accept(***); virtual void Close(); virtual int Receive(***); virtual int Send(***); }; |
对照看CAsyncSocket的定义,也是来自于afxsock.h:
class CAsyncSocket : public CObject { // 该定义中的成员变量、一些保护函数等被简化掉了 CAsyncSocket(); BOOL Create(***);
SOCKET m_hSocket;
operator SOCKET() const; BOOL Attach(***); // 我们下面将研究此函数 SOCKET Detach();
BOOL GetPeer/SockName(***); BOOL Set/GetSockOpt(); static CAsyncSocket* PASCAL FromHandle(SOCKET hSocket); // 将研究
virtual BOOL Accept(***); BOOL Bind/Ex(***); virtual void Close(); BOOL Connect/Ex(***); BOOL IOCtl(long lCommand, DWORD* lpArgument); BOOL Listen(int nConnectionBacklog=5); BOOL ShutDown(int nHow = sends); virtual int Receive/From/FromEx(void* lpBuf, int nBufLen, int nFlags = 0); virtual int Send/To/ToEx(***); BOOL AsyncSelect(long lEvent=FD_READ|FD_WRITE|FD_OOB|FD_ACCEPT|FD_CONNECT|FD_CLOSE);
virtual void OnReceive(int nErrorCode); // 下面这些 On 函数可以重载于派生类中 virtual void OnSend(int nErrorCode); virtual void OnOutOfBandData(int nErrorCode); virtual void OnAccept(int nErrorCode); virtual void OnConnect(int nErrorCode); virtual void OnClose(int nErrorCode);
BOOL Socket(***); }; |
对于Socket 的一般性使用函数,如构造,Create, Listen, Bind, Accept, Connect,我想去查看MSDN文档应该就足够了。由于CSocket从CAsyncSocket派生,所以我们重点去研究 CAsyncSocket,也就是MFC如何实现AsyncSocket的。
Socket 的创建在CAsyncSocket中实现,而在真实创建一个SOCKET句柄之后,MFC会调用AttachHandle将Socket句柄和 CAsyncSocket(或其派生的)对象附加起来,类似于CWnd和HWND句柄的关系。以下是AttachHandle的代码:
void PASCAL CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL ) { #define _afxSockThreadState AfxGetModuleThreadState() _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState; // 每线程一个,定义被复制在上一行 if (pState->m_pmapSocketHandle->IsEmpty()) { // 创建一个 CSocketWnd , 这里减去了一些代码 CSocketWnd* pWnd = new CSocketWnd; pWnd->CreateEx(. . . , _T("Socket Notification Sink"), . . .)) pState->m_hSocketWindow = pWnd->m_hWnd; } // 完成句柄到对象的映射 pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket); } |
注:为了简化,去掉了对Dead Socket处理部分的代码。
显然这里创建了一个CSocketWnd不是为了好玩才创建的,这个窗口将处理一些消息:
class CSocketWnd : public CWnd { CSocketWnd(); LRESULT OnSocketNotify(WPARAM wParam, LPARAM lParam); // 这两个消息藏着某些秘密 LRESULT OnSocketDead(WPARAM wParam, LPARAM lParam); }; |
秘密在这里(OnSocketDead也类似):
LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam) { CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam); CSocket::ProcessAuxQueue(); return 0L; } |
从这里可以得知,AsyncSocket实际上是WinSock将发生在Socket的所有消息发送给了CSocketWnd,然后CSocketWnd将这些消息组织成为一个队列,交给CSocket::ProcessAuxQueue来处理。那么有几个问题:
1、AuxQueueAdd和ProcessAuxQueue都做了些什么?
2、Socket怎么和CSocketWnd绑定在一起?
下面我们分别研究:
AuxQueueAdd代码如下,可以看出消息被加入到一个队列的末尾:
void PASCAL CSocket::AuxQueueAdd(UINT message, WPARAM wParam, LPARAM lParam) { _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState; MSG* pMsg = new MSG; pMsg->message = message; pMsg->wParam = wParam; pMsg->lParam = lParam; pState->m_plistSocketNotifications->AddTail(pMsg); // 不去看也知道这是一个队列了 } |
这个是ProcessAuxQueue:
int PASCAL CSocket::ProcessAuxQueue() { // 简化了一些代码 _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState; while(!pState->m_plistSocketNotifications->IsEmpty()) { // 取得第一个消息(先入先出的事件队列) MSG* pMsg = (MSG*)pState->m_plistSocketNotifications->RemoveHead(); if (pMsg->message == WM_SOCKET_NOTIFY) CAsyncSocket::DoCallBack(pMsg->wParam, pMsg->lParam); // 回调给 On 系列函数 else CAsyncSocket::DetachHandle((SOCKET)pMsg->wParam, TRUE); // 删除句柄对象映射 delete pMsg; } return nCount; } |
不出所料,DoCallBack回调给On系列的虚拟函数:
void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam) { // 简化掉了一些代码 switch (WSAGETSELECTEVENT(lParam)) { // 可以在 OnXXX 回调函数中进行必要的读写等操作啦 case FD_READ: pSocket->OnReceive(nErrorCode); break; case FD_WRITE: pSocket->OnSend(nErrorCode); break; case FD_OOB: pSocket->OnOutOfBandData(nErrorCode); break; case FD_ACCEPT: pSocket->OnAccept(nErrorCode); break; case FD_CONNECT: pSocket->OnConnect(nErrorCode); break; case FD_CLOSE: pSocket->OnClose(nErrorCode); break; } } |
说来说去,就和MFC将窗口句柄HWND和CWnd对象附加在一起的原理是一样的了。
Socket怎么和CSocketWnd绑定在一起?的问题还没有回答,但我们看看下面这个函数就会明白了:
BOOL CAsyncSocket::AsyncSelect(long lEvent) { _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState; WSAAsyncSelect(m_hSocket, pState->m_hSocketWindow, WM_SOCKET_NOTIFY, lEvent); } |
WSAAsyncSelect函数将Socket句柄m_hSocket和本线程的SocketWnd绑定在一起,发生事件后给窗口发送WM_SOCKET_NOTIFY事件,发生的事件为lEvent参数。
而在Create等函数中会内部调用这个函数的:
BOOL CAsyncSocket::Socket(int nSocketType, long lEvent, int, int) { // . . . AsyncSelect(lEvent) // 对感兴趣的事件产生 Windows Message 通知 // . . . } |
当然我们也可以调用,以当我们感兴趣的事情发生时,能够通过Windows Message触发重载的On系列函数。这同时也给我们在多线程环境下使用Socket提出了限制,必须了解到每个使用Socket的线程和 Socket,SocketWnd的隐含关系,你才可能在多线程Socket编成中理解为什么会出现一些奇怪的问题了。
书中也坦率地告诉我们,使用CSocket甚至CAsyncSocket并不比直接使用SOCKET强多少,书中提到多线程下可能好一点,但我认为MFC的模型会更糟。去参考一下ACE的模式也许更好。
MFC和另外一个网络编程有关的部分是WinInet,下面几个类我觉得就是对WinInet中函数/句柄的一般封装:(来自于afxinet.h)
class CInternetSession; // from CObject class CGopherLocator; // from CObject class CInternetFile; // from CStdioFile (FILETXT.CPP) class CHttpFile; class CGopherFile; class CInternetConnection; class CFtpConnection; class CGopherConnection; class CHttpConnection; class CFtpFileFind; // from CFileFind (FILEFIND.CPP) class CGopherFileFind; |
其中CInternetConnection封装一个HINTERNET句柄,代表一个网络服务器的链接,可以是HTTP/FTP/GOPHER三种协议之一(在派生类中具体实现)。
我们以CHttpConnection为例:
class CHttpConnection : public CInternetConnection { // 一个典型的 HTTP 协议的网络连接构造 CHttpConnection(CInternetSession* pSession, LPCTSTR pstrServer, DWORD dwFlags, INTERNET_PORT nPort = INTERNET_INVALID_PORT_NUMBER, LPCTSTR pstrUserName = NULL, LPCTSTR pstrPassword = NULL, DWORD_PTR dwContext = 1); // 以指定 Verb 打开指定 URL 的资源,一般是 GET 或 POST CHttpFile* OpenRequest(LPCTSTR pstrVerb, LPCTSTR pstrObjectName(URL), LPCTSTR pstrReferer = NULL,DWORD_PTR dwContext = 1, LPCTSTR* ppstrAcceptTypes = NULL, LPCTSTR pstrVersion = NULL, DWORD dwFlags = INTERNET_FLAG_EXISTING_CONNECT); } |
打开的CHttpFile类似于普通文件:
class CInternetFile : public CStdioFile class CHttpFile : public CInternetFile |
可以在这个文件上进行读取/写入等操作,但是我怀疑没有异步访问模式的支持下能否真实的开发出可靠的系统来?
微软为VB开发者还提供了一个Microsoft Internet Transfer Control控件,基本上也是对WinInet的一个ActiveX级别的封装,我想用这个都比用MFC的那些类要强。
再来看看ISAPI,我用MFC编写过一个ISAPI的扩展动态连接库,说起来真不光彩,没有明白MFC怎么封装ISAPI Extension模式的。现在也不打算仔细研究,大概了解就行了,估计不会有人愿意用C++来开发网站的,除非。。。
不过为了学习的完整性,还是简单看看吧:(定义于afxisapi.h)
class CHtmlStream; class CHttpServerContext; class CHttpServer; class CHttpFilterContext; class CHttpFilter; class CHttpArgList; struct CHttpArg; |
看来MFC对编写ISAPI Extension DLL和Filter DLL都还有支持,先看看Filter。
类CHttpFilterContext对HTTP_FILTER_CONTEXT进行了简单的封装,不过我发现在类的内部HTTP_FILTER_CONTEXT定义为:
PHTTP_FILTER_CONTEXT const m_pFC; |
?意思是不能修改HTTP_FILTER_CONTEXT里面的东西?
CHttpFilter中包含两个和ISAPI Filter中对应的函数(新版本的ISAPI Filter中还有第三个),HttpFilterProc, GetFilterVersion,然后就是一组OnXXX分发函数,MFC的类好像都是这个样子:
class CHttpFilter { CHttpFilter(); virtual DWORD HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD, LPVOID); virtual BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer); // 下面好多个 OnXXX 的重载函数,是从 HttpFilterProc 中分发出来的,可以重载 virtual DWORD OnUrlMap(CHttpFilterContext* pfc, PHTTP_FILTER_URL_MAP pUrlMap); }; |
现在问题是,ISAPI Filter DLL最终的形态是一个DLL,并且拥有2(3)个入口函数:GetFilterVersion, HttpFilterProc, TerminateFilter,在DLL中这些入口函数是怎样“变为”对象里面的对应函数的?看这里:
extern "C" DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD dwNotificationType, LPVOID pvNotification) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); DWORD dwRet; if (pFilter == NULL) dwRet = SF_STATUS_REQ_NEXT_NOTIFICATION; else dwRet = pFilter->HttpFilterProc(pfc, dwNotificationType, pvNotification); return dwRet; } |
其中pFilter是这样的:
static CHttpServer* pServer = NULL; // ISAPI Extension DLL 也是类似方式 static CHttpFilter* pFilter = NULL; // 全局的 Singleton 对象 |
这回没有再用MFC常用的HANDLE-OBJECT MAP机制了:)
再来看看CHttpServer是怎样实现ISAPI Extension DLL的:
class CHttpServer { CHttpServer(TCHAR cDelimiter = '&'); virtual BOOL OnParseError(CHttpServerContext* pCtxt, int nCause); virtual BOOL OnWriteBody(CHttpServerContext* pCtxt, LPBYTE, DWORD, DWORD); virtual BOOL TerminateExtension(DWORD dwFlags); virtual DWORD HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB); virtual BOOL GetExtensionVersion(HSE_VERSION_INFO *pVer); // 其它还有好多好多函数,简化掉 } |
这回有TerminateExtension函数了,为什么Filter没有?:)
去看看最重要的入口函数HttpExtensionProc都做些什么?
DWORD CHttpServer::HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) { // 前面有很多验证,简化掉 // 获取 HTTP 请求的信息,并验证 // 开始干活 pServer->InitInstance(&ctxtCall); pServer->CallFunction(&ctxtCall, pszQuery, pszCommand); // 忽略了大部分错误清理判定等 } |
CallFunction是这样的:
int CHttpServer::CallFunction(CHttpServerContext* pCtxt, LPTSTR pszQuery, LPTSTR pszCommand) { // 首先在 PARSEMAP 中查找使用了什么命令(pszMethod),找到对应的处理命令(pFn) pFn = LookUp(pszMethod, pMap, pParams); // 调用这个命令的处理器函数 nRet = CallMemberFunc(pCtxt, pFn, pParams, pszParams); // 中间大堆的复杂处理被简化掉,然后这里返回 nRet } |
CallMemberFunc 居然也挺复杂的,大致来说,是将参数pszParams压入堆栈(PushStackArgs),然后调用_AfxParseCall函数完成实际的成员 函数调用,它是一小段汇编代码组成的,如果有兴趣可以研究inetcall.cpp来看看该函数怎么处理堆栈和调用的。这一过程完成从Extension DLL的命令到函数的映射,也就是派生CHttpServer时用宏BEGIN_PARSE_MAP, ON_PARSE_COMMAND, ON_PARSE_COMMAND_PARAMS, END_PARSE_MAP包装的命令处理器。
MFC CHttpServer辛勤的完成了URL字符串解析,分发到要处理的函数上面,包装你的输出到可以方便使用的CHtmlStream里面,不过真的需要 用C++来写扩展的机会并不是很多,而且即使要写,MFC也不一定是好的选择,它太大太慢了。。。,而且现在的其它选择也很多。。。。。。