📄 mfc教程_ socket类的设计和实现.htm
字号:
<P align=justify>Close();</P>
<P align=justify>WSASetLastError(nResult);</P>
<P align=justify>}</P>
<P align=justify>return FALSE;</P>
<P align=justify>}</P>
<P align=justify>其中:</P>
<P align=justify>参数1表示本socket的端口,缺省是0,如果要创建数据报的socket,则必须指定一个端口号。</P>
<P align=justify>参数2表示本socket的类型,缺省是SOCK_STREAM,表示面向连接类型。</P>
<P align=justify>参数3是屏蔽位,表示希望对本socket监测的事件,缺省是FD_READ | FD_WRITE | FD_OOB
| FD_ACCEPT | FD_CONNECT | FD_CLOSE。</P>
<P align=justify>参数4表示本socket的IP地址字符串,缺省是NULL。</P>
<P
align=justify>Create调用Socket函数创建一个socket,并把它捆绑在this所指对象上,监测指定的网络事件。参数2和3被传递给Socket函数,如果希望创建数据报的socket,不要使用缺省参数,指定参数2是SOCK_DGRM。</P>
<P align=justify>如果上一步骤成功,则调用bind给新的socket分配端口和IP地址。</P>
<P align=justify>(2)Socket函数</P>
<P align=justify>接着,分析Socket函数,其实现如下:</P>
<P align=justify>BOOL CAsyncSocket::Socket(int nSocketType, long
lEvent,</P>
<P align=justify>int nProtocolType, int nAddressFormat)</P>
<P align=justify>{</P>
<P align=justify>ASSERT(m_hSocket == INVALID_SOCKET);</P>
<P align=justify></P>
<P align=justify>m_hSocket =
socket(nAddressFormat,nSocketType,nProtocolType);</P>
<P align=justify>if (m_hSocket != INVALID_SOCKET)</P>
<P align=justify>{</P>
<P align=justify>CAsyncSocket::AttachHandle(m_hSocket, this, FALSE);</P>
<P align=justify>return AsyncSelect(lEvent);</P>
<P align=justify>}</P>
<P align=justify>return FALSE;</P>
<P align=justify>}</P>
<P align=justify>其中:</P>
<P align=justify>参数1表示Socket类型,缺省值是SOCK_STREAM。</P>
<P align=justify>参数2表示希望监测的网络事件,缺省值同Create,指定了全部事件。</P>
<P
align=justify>参数3表示使用的协议,缺省是0。实际上,SOCK_STREAM类型的socket使用TCP协议,SOCK_DGRM的socket则使用UDP协议。</P>
<P
align=justify>参数4表示地址族(地址格式),缺省值是PF_INET(等同于AF_INET)。对于TCP/IP来说,协议族和地址族是同值的。</P>
<P
align=justify>在socket没有被创建之前,成员变量m_hSocket是一个无效的socket句柄。Socket函数把协议族、socket类型、使用的协议等信息传递给WinSock
API函数socket,创建一个socket。如果创建成功,则把它捆绑在this所指对象。</P>
<P align=justify>(3)捆绑(Attatch)</P>
<P
align=justify>捆绑过程类似于其他Windows对象,将在模块线程状态的WinSock映射中添加一对新的映射:this所指对象和新创建的socket对象的映射。</P>
<P
align=justify>另外,如果本模块线程状态的“socket窗口”没有创建,则创建一个,该窗口在异步操作时用来接收WinSock的通知消息,窗口句柄保存到模块线程状态的m_hSocketWindow变量中。函数AsyncSelect将指定该窗口为网络事件消息的接收窗口。</P>
<P align=justify>函数AttachHandle的实现在此不列举了。</P>
<P align=justify>(4)指定要监测的网络事件</P>
<P
align=justify>在捆绑完成之后,调用AsyncSelect指定新创建的socket将监测的网络事件。AsyncSelect实现如下:</P>
<P align=justify>BOOL CAsyncSocket::AsyncSelect(long lEvent)</P>
<P align=justify>{</P>
<P align=justify>ASSERT(m_hSocket != INVALID_SOCKET);</P>
<P align=justify></P>
<P align=justify>_AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;</P>
<P align=justify>ASSERT(pState->m_hSocketWindow != NULL);</P>
<P align=justify></P>
<P align=justify>return WSAAsyncSelect(m_hSocket,
pState->m_hSocketWindow,</P>
<P align=justify>WM_SOCKET_NOTIFY, lEvent) != SOCKET_ERROR;</P>
<P align=justify>}</P>
<P align=justify>函数参数lEvent表示希望监视的网络事件。</P>
<P align=justify>_ afxSockThreadState得到的是当前的模块线程状态,m_
hSocketWindow是本模块在当前线程的“socket窗口”,指定监视m_hSocket的网络事件,如指定事件发生,给窗口m_hSocketWindow发送WM_SOCKET_NOTIFY消息。</P>
<P
align=justify>被指定的网络事件对应的网络I/O将是异步操作,是非阻塞操作。例如:指定FR_READ导致Receive是一个异步操作,如果不能立即读到数据,则返回一个错误WSAEWOULDBLOCK。在数据到达之后,WinSock通知窗口m_hSocketWindow,导致OnReceive被调用。</P>
<P
align=justify>指定FR_WRITE导致Send是一个异步操作,即使数据没有送出也返回一个错误WSAEWOULDBLOCK。在数据可以发送之后,WinSock通知窗口m_hSocketWindow,导致OnSend被调用。</P>
<P
align=justify>指定FR_CONNECT导致Connect是一个异步操作,还没有连接上就返回错误信息WSAEWOULDBLOCK,在连接完成之后,WinSock通知窗口m_hSocketWindow,导致OnConnect被调用。</P>
<P align=justify>对于其他网络事件,就不一一解释了。</P>
<P
align=justify>所以,使用CAsyncSocket时,如果使用Create缺省创建socket,则所有网络I/O都是异步操作,进行有关网络I/O时则必须覆盖以下的相关函数:</P>
<P
align=justify>OnAccept、OnClose、OnConnect、OnOutOfBandData、OnReceive、OnSend。</P>
<P align=justify>(5)Bind函数</P>
<P
align=justify>经过上述过程,socket创建完毕,下面,调用Bind函数给m_hSocket指定本地端口和IP地址。Bind的实现如下:</P>
<P align=justify>BOOL CAsyncSocket::Bind(UINT nSocketPort, LPCTSTR
lpszSocketAddress)</P>
<P align=justify>{</P>
<P align=justify>USES_CONVERSION;</P>
<P align=justify></P>
<P align=justify>//使用WinSock的地址结构构造地址信息</P>
<P align=justify>SOCKADDR_IN sockAddr;</P>
<P align=justify>memset(&sockAddr,0,sizeof(sockAddr));</P>
<P align=justify></P>
<P align=justify>//得到地址参数的值</P>
<P align=justify>LPSTR lpszAscii = T2A((LPTSTR)lpszSocketAddress);</P>
<P align=justify>//指定是Internet地址类型</P>
<P align=justify>sockAddr.sin_family = AF_INET;</P>
<P align=justify></P>
<P align=justify>if (lpszAscii == NULL)</P>
<P align=justify>//没有指定地址,则自动得到一个本地IP地址</P>
<P align=justify>//把32比特的数据从主机字节序转换成网络字节序</P>
<P align=justify>sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);</P>
<P align=justify>else</P>
<P align=justify>{</P>
<P align=justify>//得到地址</P>
<P align=justify>DWORD lResult = inet_addr(lpszAscii);</P>
<P align=justify>if (lResult == INADDR_NONE)</P>
<P align=justify>{</P>
<P align=justify>WSASetLastError(WSAEINVAL);</P>
<P align=justify>return FALSE;</P>
<P align=justify>}</P>
<P align=justify>sockAddr.sin_addr.s_addr = lResult;</P>
<P align=justify>}</P>
<P align=justify></P>
<P align=justify>//如果端口为0,则WinSock分配一个端口(1024—5000)</P>
<P align=justify>//把16比特的数据从主机字节序转换成网络字节序</P>
<P align=justify>sockAddr.sin_port = htons((u_short)nSocketPort);</P>
<P align=justify></P>
<P align=justify>//Bind调用WinSock API函数bind</P>
<P align=justify>return Bind((SOCKADDR*)&sockAddr,
sizeof(sockAddr));</P>
<P align=justify>}</P>
<P align=justify>其中:函数参数1指定了端口;参数2指定了一个包含本地地址的字符串,缺省是NULL。</P>
<P
align=justify>函数Bind首先使用结构SOCKADDR_IN构造地址信息。该结构的域sin_family表示地址格式(TCP/IP同协议族),赋值为AF_INET(Internet地址格式);域sin_port表示端口,如果参数1为0,则WinSock分配一个端口给它,范围在1024和5000之间;域sin_addr是表示地址信息,它是一个联合体,其中s_addr表示如下形式的字符串,“28.56.22.8”。如果参数没有指定地址,则WinSock自动地得到本地IP地址(如果有几个网卡,则使用其中一个的地址)。</P>
<P align=justify>(6)总结Create的过程</P>
<P
align=justify>首先,调用socket函数创建一个socket;然后把创建的socket对象映射到CAsyncSocket对象(捆绑在一起),指定本socket要通知的网络事件,并创建一个“socket窗口”来接收网络事件消息,最后,指定socket的本地信息。</P>
<P
align=justify>下一步,是使用成员函数Connect连接远地主机,配置socket的远地信息。函数Connect类似于Bind,把指定的远地地址转换成SOCKADDR_IN对象表示的地址信息(包括网络字节序的转换),然后调用WinSock函数Connect连接远地主机,配置socket的远地端口和远地IP地址。</P>
<P align=justify></P>
<LI><A name=_Toc452641021></A><A name=_Toc457299161></A><B>异步网络事件的处理</B>
<P></P></LI></OL>
<P
align=justify>当网络事件发生时,“socket窗口”接收WM_SOCKET_NOTIFY消息,消息处理函数OnSocketNotify被调用。“socket窗口”的定义和消息处理是MFC实现的,这里不作详细的讨论。</P>
<P
align=justify>OnSocketNotify回调CAsyncSocket的成员函数DoCallBack,DoCallBack调用事件处理函数,如OnRead、OnWrite等。摘录DoCallBack的一段代码如下:</P>
<P align=justify>switch (WSAGETSELECTEVENT(lParam))</P>
<P align=justify>{</P>
<P align=justify>case FD_READ:</P>
<P align=justify>{</P>
<P align=justify>DWORD nBytes;</P>
<P align=justify>//得到可以一次读取的字节数</P>
<P align=justify>pSocket->IOCtl(FIONREAD, &nBytes);</P>
<P align=justify>if (nBytes != 0)</P>
<P align=justify>pSocket->OnReceive(nErrorCode);</P>
<P align=justify>}</P>
<P align=justify>break;</P>
<P align=justify>case FD_WRITE:</P>
<P align=justify>pSocket->OnSend(nErrorCode);</P>
<P align=justify>break;</P>
<P align=justify>case FD_OOB:</P>
<P align=justify>pSocket->OnOutOfBandData(nErrorCode);</P>
<P align=justify>break;</P>
<P align=justify>case FD_ACCEPT:</P>
<P align=justify>pSocket->OnAccept(nErrorCode);</P>
<P align=justify>break;</P>
<P align=justify>case FD_CONNECT:</P>
<P align=justify>pSocket->OnConnect(nErrorCode);</P>
<P align=justify>break;</P>
<P align=justify>case FD_CLOSE:</P>
<P align=justify>pSocket->OnClose(nErrorCode);</P>
<P align=justify>break;</P>
<P align=justify></P>
<P
align=justify>lParam是WM_SOCKET_NOFITY的消息参数,OnSocketNotify传递给函数DoCallBack,表示通知事件。</P>
<P
align=justify>函数IOCtl是CAsyncSocket的成员函数,用来对socket的I/O进行控制。这里的使用表示本次调用Receive函数至多可以读nBytes个字节。</P>
<P align=justify>从上面的讨论可以看出,从创建socket到网络I/O,CAsyncSocket直接封装了低层的WinSock
API,简化了WinSock编程,实现了一个异步操作的界面。如果希望某个操作是阻塞操作,则在调用Create时不要指定该操作对应的网络事件。例如,希望Connect和Send是阻塞操作,在任务完成之后才返回,则可以使用如下的语句:</P>
<P align=justify>pSocket->Create(0, SOCK_STREAM, </P>
<P align=justify>FR_WRITE|FR_OOB|FR_ACCEPT|FR_CLOSE);</P>
<P
align=justify>这样,在Connect和Send时,如果是用户界面线程的话,可能阻塞线程消息循环。所以,最好在工作者线程中使用阻塞操作。</P>
<P align=justify></P>
<LI><A name=_Toc452641022></A><A name=_Toc457299162></A><B>CSocket</B>
<P></P>
<P
align=justify>如果希望在用户界面线程中使用阻塞socket,则可以使用CSocket。它在非阻塞socket基础之上实现了阻塞操作,在阻塞期间实现了消息循环。</P>
<P
align=justify>对于CSocket,处理网络事件通知的函数OnAccept、OnClose、OnReceive仍然可以使用,OnConnect、OnSend在CSocket中永远不会被调用,另外OnOutOfBandData在CSocket中不鼓励使用。</P>
<P
align=justify>CSocket对象在调用Connect、Send、Accept、Close、Receive等成员函数后,这些函数在完成任务之后(连接被建立、数据被发送、连接请求被接收、socket被关闭、数据被读取)之后才会返回。因此,Connect和Send不会导致OnConnect和OnSend被调用。如果覆盖虚拟函数OnReceive、OnAccept、OnClose,不主动调用Receive、Accept、Close,则在网络事件到达之后导致对应的虚拟函数被调用,虚拟函数的实现应该调用Receive、Accept、Close来完成操作。下面,就一个函数Receive来考察CSocket如何实现阻塞操作和消息循环的。</P>
<P align=justify>int CSocket::Receive(void* lpBuf, int nBufLen, int
nFlags)</P>
<P align=justify>{</P>
<P align=justify>//m_pbBlocking是CSocket的成员变量,用来标识当前是否正在进行</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -