[接上篇]
封装目标: 最终目标是封装WinSock的WSAAsyncSelect IO模型。
封装原则: 耦合性[减少各种依赖,包括classes之间,编译模块之间。],小粒度增加可复用性。
依赖ATL/WTL。
CKxAsyncSocket 是我要实现的class,它只需要维护SOCKET成员,另外因为是异步选择,所以可以再维护一个HWND。
关于HWND,是否有必要暴露给用户,关键在于Socket要仅仅运行于UI主线程,还是用户可以自己创建另一个有消息循环的Thread,在其上处理Socket的消息。
我选择后者,因为,尽管异步的Socket,在Recv大数据的时候,还是可能导致UI“卡”的。
那么,我们就需要一个线程,一个有处理消息的窗口,二者形成一个UI线程, 然后再该UI线程处理Socket消息。以上提到的几个实体,实体之间要尽量低耦合,无依赖。
【通常来说,接口耦合和可以接受的,我们也会通过一些技巧来实现低耦合】
1. 线程只需要一个简单的CreateThread封装,Like Java Style的就好,支持Runnable接口,好处就不说了。
2. 处理消息的窗口,我们用WTL,搞个简单的:
{ \
if( NULL != theChainMemberPtr && \
theChainMemberPtr->ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult)) \
return TRUE; \
}
class MessageOnlyWindow : public CWindowImpl<MessageOnlyWindow, CWindow, CWinTraits<> >
{
public:
BEGIN_MSG_MAP(MessageOnlyWindow)
CHAIN_MSG_MAP_MEMBER_PTR( m_pWinMsgHandler )
END_MSG_MAP()
MessageOnlyWindow(){}
~MessageOnlyWindow()
{
::DestroyWindow(m_hWnd);
m_hWnd = NULL;
}
HWND Create(ProcessWindowMessageHandler* pWinMsgHandler)
{
m_pWinMsgHandler = pWinMsgHandler;
return CWindowImpl<MessageOnlyWindow, CWindow, CWinTraits<> >::Create(HWND_MESSAGE);
}
private:
ProcessWindowMessageHandler* m_pWinMsgHandler;
};
需要说明一下:
CHAIN_MSG_MAP_MEMBER_PTR是仿WTL的宏,WTL缺少一个处理成员指针的CHAIN*. 这样HWND_MESSAGE窗口的消息,都可以投递给ProcessWindowMessageHandler* m_pWinMsgHandler了。PS:
{
public:
virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID = 0) = 0;
};
// ProcessWindowMessage和BEGIN_MSG_MAP的声明是一致的,但是处理成虚函数了。这样可以多态到派生类,但是又不会依赖派生类(的声明)
// WTL大量依赖模板来处理这种关系。
那么现在,我们再创建一个类,表达UI Thread的概念即可,它可以继承或者聚合一个CKxThread即可。然后在线程中创建窗口和消息循环即可:
{
MessageOnlyWindow msgWnd;
HWND hWnd = msgWnd.Create(m_pRoutine);
m_pRoutine->OnBegin( pThread, hWnd );
MSG msg;
while( GetMessage(&msg, NULL, 0, 0) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return 0;
} // 代码有节略.
再看下m_pRoutine的类型:
实际上用户自己派生的
IKxMessageRoutine,就可以通过Windows Message Thread实例传递给MessageOnlyWindow。这样用户的派生类就可以直接Handle这个Thread的消息了,看一下这个派生类的样子:{
private:
BEGIN_MSG_MAP(CUserThreadMessageHandler)
CHAIN_MSG_MAP(CKxAsyncSocketMessageMixin)
END_MSG_MAP()
首先,我们可以确定的是,
CUserThreadMessageHandler仅仅是消息的处理者,而且和Socket事件没有必然联系,如果不继承CKxAsyncSocketMessageMixin(忽略此处的Public继承)的话。因为Windows中,除了异步选择的Socket IO模型,还有很多依赖消息循环的编程模型。所以不考虑第二个继承关系的CUserThreadMessageHandler是一个很好的处理UI 线程消息的对象。在该线程中,需要处理的消息,可以按照WTL的消息映射写处理函数。那么,我们现在要处理Socket的事件了,好,加一个Mixin即可【当然了,要通过
CHAIN_MSG_MAP来把CKxAsyncSocketMessageMixin接入到消息队列中】,这样,一个Socket绑定了这个线程的消息窗口后,就可以在这个线程的消息循环中工作了。CKxAsyncSocketMessageMixin是一个处理Socket事件的类,并且把SOCKET<->Socket对象指针进行映射【没办法,OS投递SOcket事件的时候,参数是SOCKET,而不是别的,所以自己要做这个Mapping,搞过WinSock封装的人,都清楚这点。HWND<->CWnd*的映射也是一般道理。】
{
public:
BEGIN_MSG_MAP(CKxAsyncSocketMessageMixin)
MESSAGE_HANDLER(HK_WM_SOCKET, OnSocketMessage)
... ...
// HK_WM_SOCKET就是WinSock API WSAAsyncSelect的第三个参数,你懂的。
最后,Socket对象也就可以收到消息了。然后根据消息转到不同的处理函数即可,OnConnect,OnRecv...[并且我的Socket,也实行了ProcessWindowMessageHandler接口]
-------------------------------------------------------------------------------------------------------------------------------------------------------
// Socket 本身相关的.
[WinSock]其实和WTL关系不大,但是我会用到一些WTL的东西。
因为是异步选择,所以需要一个窗口来接收消息,于是...
{
BEGIN_MSG_MAP(MessageOnlyWindow)
MESSAGE_HANDLER(WM_SOCKET, OnSocketMessage)
END_MSG_MAP()
HWND Create()
}
};
这样,非常简单的搞定了消息窗口【可算是和WTL扯上关系了】
异步Socket的Receive才是我关注的:
[1]
WSAAsyncSelect自动把一个阻塞的socket转为非阻塞的,如果需要转为阻塞的,那么先要调用这个函数,并且(最后一个参数long lEvent设置为0) 。然后调用ioctlsocket,或者WSAIoctl。
// MSDN:The WSAAsyncSelect function automatically sets socket s to nonblocking mode, regardless of the value of lEvent. To set socket s back to blocking mode, it is first necessary to clear the event record associated with socket s via a call to WSAAsyncSelect with lEvent set to zero. You can then call ioctlsocket or WSAIoctl to set the socket back to blocking mode.
设置为阻塞的:
ioctlsocket( m_hSocket, FIONBIO, &iMode );
[2]
一旦设置了阻塞,我想,还是加个超时的好:
setsockopt( m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&dwRecvTimeout, sizeof(DWORD) );