注:本文适合有一定Winsock编程和mfc编程基础的朋友阅读。
我是一个对一切事情都爱探究到底的人,前一段由于需要使用mfc开发一个网络通信程序,于是就顺便研究了一下CSocket类的工作工程,现将我的一些学习成果公布出来,希望大家多多指正。
先看CSocket的create函数,它调用了基类CAsyncSocket::Create函数,下面跟进去看到
BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType,
    long lEvent, LPCTSTR lpszSocketAddress)
{
    if (Socket(nSocketType, lEvent))
    {
        if (Bind(nSocketPort,lpszSocketAddress))
            return TRUE;
        int nResult = GetLastError();
        Close();
        WSASetLastError(nResult);
    }
    return FALSE;
}
先调用Socket函数,在此函数中先调用api函数socket创建一个套接字并返回给m_hSocket,如果创建成功就进行CAsyncSocket::AttachHandle,实现套接字句柄与CSocket类对象的绑定,在绑定函数中创建了一个CSocketWnd对象,并创建一个窗口但并不显示,我称之为socket窗口,在创建之前有个判断pState->m_pmapSocketHandle->IsEmpty(),作用是检查socket的绑定映射是否为空,如果是空就不再创建socket窗口了,目的是节省资源,也就是保证只有一个socket窗口。socket窗口用于接收网络消息,并处理消息响应。接下来调用CAsyncSocket::AsyncSelect函数,其中又调用了WSAAsyncSelect函数,创建异步套接字,即当一些网络行为发生时向socket窗口发送WM_SOCKET_NOTIFY消息,然后消息处理程序在进行分类处理。这是CSocket类工作的核心。 这里注册了这些事件FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE,详细含义大家可参考msdn。即这鞋事件发生时向socket窗口发送WM_SOCKET_NOTIFY消息。接下来的Bind函数无非就是做一些初始化并调用api函数bind,如果有一些基础大家都能明白。如果出错就关闭套接字,成功就返回。
接下来按标准过程就该监听调用CSocket::Listen函数,此函数很简单,就是调用api函数listen。
下面我来说核心的另一方面,消息处理过程。socket窗口接到WM_SOCKET_NOTIFY消息,根据消息映射,调用CSocketWnd::OnSocketNotify,其中调用静态函数CSocket::ProcessAuxQueue,经过一些判断保护,调用了CAsyncSocket::DoCallBack,同样他也是个静态函数。
这个函数最关键,我把代码放上来。详细解释我放在注释里。
void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
{
    if (wParam == 0 && lParam == 0)
        return;
 
        CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE);//根据WM_SOCKET_NOTIFY消息的wParam参数寻找对应CSocket对象,这是一个保护,如果对象为空直接返回。因为在异步模式下,每发出一回动作,就会发出WM_SOCKET_NOTIFY消息,如果连续发出读操作,然后关闭套接字。那么当处理多余的消息时套接字已不存在,那么就直接返回。
 
        if (pSocket != NULL)
        return;
 
    pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, FALSE);
    if (pSocket == NULL)
    {
                pSocket = CAsyncSocket::LookupHandle(INVALID_SOCKET, FALSE);
        ASSERT(pSocket != NULL);
 
        if(pSocket == NULL)
            return;
 
        pSocket->m_hSocket = (SOCKET)wParam;
        CAsyncSocket::DetachHandle(INVALID_SOCKET, FALSE);
        CAsyncSocket::AttachHandle(pSocket->m_hSocket, pSocket, FALSE);
    }
 
    int nErrorCode = WSAGETSELECTERROR(lParam);
    switch (WSAGETSELECTEVENT(lParam))//根据lParam的内容分类处理不同事件的操作,并在其中调用程序员自己编写的处理函数如OnReceive等。
    {
    case FD_READ:
        {
            fd_set fds;
            int nReady;
            timeval timeout;
 
            timeout.tv_sec = 0;
            timeout.tv_usec = 0;
 
            FD_ZERO(&fds);
            FD_SET(pSocket->m_hSocket, &fds);
            nReady = select(0, &fds, NULL, NULL, &timeout);
            if (nReady == SOCKET_ERROR)
                nErrorCode = WSAGetLastError();
            if ((nReady == 1) || (nErrorCode != 0))
                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;
    }
}
 
在CSocket::Accept函数即接受连接函数中,就是调用api函数accept这么简单。
下面一点也比较关键,也就是在异步套接字中进行操作recevie等操作时如何实现同步即阻塞。
看代码:
int CSocket::Receive(void* lpBuf, int nBufLen, int nFlags)
 
{
 
//m_pbBlocking是CSocket的成员变量,用来标识当前是否正在进行
 
//阻塞操作。但不能同时进行两个阻塞操作。
 
if (m_pbBlocking != NULL)
 
{
 
WSASetLastError(WSAEINPROGRESS);
 
return FALSE;
 
}
 
//完成数据读取
 
int nResult;
 
while ((nResult = CAsyncSocket::Receive(lpBuf, nBufLen, nFlags)) 
 
== SOCKET_ERROR)
 
{
 
if (GetLastError() == WSAEWOULDBLOCK)
 
{
 
//进入消息循环,等待网络事件FD_READ
 
if (!PumpMessages(FD_READ))
 
return SOCKET_ERROR;
 
}
 
else
 
return SOCKET_ERROR;
 
}
 
return nResult;
 
}
Receive函数首先判断当前CSocket对象是否正在处理一个阻塞操作,如果是,则返回错误WSAEINPROGRESS;否则,开始数据读取的处理。读取数据时,如果基类CAsyncSocket的Receive读取到了数据,则返回;否则,如果返回一个错误,而且错误号是WSAEWOULDBLOCK,则表示操作阻塞,于是调用PumpMessage进入消息循环等待数据到达(网络事件FD_READ发生)。数据到达之后退出消息循环,再次调用CAsyncSocket的Receive读取数据,直到没有数据可读为止。PumpMessages函数中其实就是不断调用PeekMessage函数,直到取到希望的消息时返回。
当CSocket对象析构时会自动调用Close函数关闭套接字或主动关闭。其实其中调用的是CAsyncSocket::Close,又调用CAsyncSocket::KillSocket,其中又调用CAsyncSocket::DetachHandle将对象与套接字分离,并关闭套接字。
好吧,这些已经将CSocket类讲的差不多了,其中的几个关键点也讲明白了,希望大家多多关注,谢谢大家的支持,初次写文章,希望大家多多谅解。