网路编程技术笔记

WinINet

1.关于WinINet

WinINet不是给服务端用的,服务端用Microsoft Windows HTTP Services (WinHTTP)
WinINet抽象了Gopher,FTP,HTTP协议的一些细节。
2.HINTERNET 句柄
WinINet函数创建、使用的句柄都是HINTERNET类型的,这种类型的句柄无法被转换成其 它类型的句柄。换句话说,最好别用ReadFile、CloseHandle之类的函数来操作这些句柄。同样的,也别用WinINet函数来访问、操作其 他类型的句柄。比如,用InternetReadFile访问CreateFile创建句柄是无法得到你想要的结果的。想关闭HINTERNET句柄要使 用InternetCloseHandle函数。
3.句柄架构


InternetOpen 创建的句柄在顶层,由接下来的一层的 InternetOpenUrl 和 InternetConnect 使用,而 InternetConnect 创建的句柄又被之后的几个函数使用。

下面这张图是依赖 InternetOpenUrl 创建的句柄的几个函数,灰色的方框是返回 HINTERNET 句柄的函数,而白色的框就是使用被创建的 HINTERNET 句柄的函数


FTP Hierarchy




HTTP Hierarchy




注意这张图,这张图的意思是 HttpSendRequestEx 先访问 HttpOpenRequest 创建的句柄之后, HttpEndRequestInternetReadFileEx 和InternetWriteFile 才能访问这个句柄。 HttpEndRequest 被调用之后,才轮的到InternetReadFileInternetSetFilePointer 和InternetQueryDataAvailable 来访问这个句柄。

4.内容编码

HTTP 协议 (RFC 2616) 规定了应用程序可以要求服务器用编码的方式(encoded format)返回HTTP响应。在Windows Server 2008 与 Windows Vista之前,发送给应用程序的内容编码了的请求需要应用程序自己处理,从Windows Server 2008 and Windows Vista开始, 应用程序可以让 WinINet 来解码了(gzip与deflate)。有三种方式开启解码选项(基于会话、请求、连接),它们的作用域不同。可以使用InternetOpen(基于会话), InternetConnect(基于连接),HttpOpenRequest(基于请求)返回的句柄调用InternetSetOption 来打开或关闭解码选项,打开则将 dwOption 参数中INTERNET_OPTION_HTTP_DECODING 选项打开, 令 lpBuffer 指向一个为true的boolean变量. 关闭则dwOption 参数中INTERNET_OPTION_HTTP_DECODING 选项打开, 令 lpBuffer 指向一个为false的boolean变量 .

设置解码选项之后, WinInet 在你调用 InternetReadFile 是就会执行一次解码。不过就算你打开了解码选项,它也不一定就为你解码……当这种情况发生时,InternetReadFile 函数会失败并返回ERROR_INTERNET_DECODING_FAILED.这个时候你可以选择去掉Accept-Encoding头重新发送一次请求,或者把解码关掉然后自己来解码(这时你就得检查Content-Encoding头来判断编码方式了)。

5.协议无关函数

FunctionDescription
InternetFindNextFile继续文件的枚举或搜索. 需要以下函数创建的句柄 FtpFindFirstFileGopherFindFirstFileInternetOpenUrl
InternetLockRequestFile允许用户锁定文件. 需要以下函数创建的句柄 FtpOpenFileGopherOpenFileHttpOpenRequestInternetOpenUrl .
InternetQueryDataAvailable查询可用数据的数量. 需要以下函数创建的句柄 FtpOpenFileGopherOpenFileHttpOpenRequest .
InternetQueryOption查询 Internet 设置.
InternetReadFile读取 URL 数据. 需要以下函数创建的句柄 InternetOpenUrlFtpOpenFileGopherOpenFileHttpOpenRequest .
InternetSetFilePointer设置文件指针. 需要以下函数创建的句柄 InternetOpenUrl ( HTTP URL only) HttpOpenRequest (GET 方法).
InternetSetOption配置 Internet 设置.
InternetSetStatusCallback设置一个接收状态信息的回调函数. 分配一个回调函数给指定的 HINTERNET 句柄及从其演化而来的句柄.
InternetUnlockRequestFile解锁被 InternetLockRequestFile 锁定的文件.
读文件

函数 InternetReadFile 用来从一个由函数 InternetOpenUrlFtpOpenFileGopherOpenFileHttpOpenRequest 返回的 HINTERNET 句柄下载资源.

WinINet 提供了两种方法来下载整个资源

寻找文件

先使用 FtpFindFirstFileGopherFindFirstFile, 或 InternetOpenUrl ,然后将其返回的句柄作为参数 传递给 InternetFindNextFile 进行继续查找,持续调用 InternetFindNextFile 知道返回扩展的错误信息 ERROR_NO_MORE_FILES 来完成整个搜索,调用 GetLastError 来获取最后的错误信息.

6.HTTP 会话


使用 WinINet 函数访问WWW资源
(1)初始化 WWW 连接
将服务类型设为 INTERNET_SERVICE_HTTP 调用 InternetConnect 来建立一个 HTTP 会话
HINTERNET InternetConnect(
__in HINTERNET hInternet, //InternetOpen 返回的句柄
__in LPCTSTR lpszServerName, //可以描述目标服务器的字符串
__in INTERNET_PORT nServerPort,//目标服务器的端口
__in LPCTSTR lpszUsername,//用户名
__in LPCTSTR lpszPassword,//密码
__in DWORD dwService,//使用的服务类型
__in DWORD dwFlags,
__in DWORD_PTR dwContext
);
nServerPort
ValueMeaning

INTERNET_DEFAULT_FTP_PORT

Uses the default port for FTP servers (port 21).

INTERNET_DEFAULT_GOPHER_PORT

Uses the default port for Gopher servers (port 70).

INTERNET_DEFAULT_HTTP_PORT

Uses the default port for HTTP servers (port 80).

INTERNET_DEFAULT_HTTPS_PORT

Uses the default port for Secure Hypertext Transfer Protocol (HTTPS) servers (port 443).

INTERNET_DEFAULT_SOCKS_PORT

Uses the default port for SOCKS firewall servers (port 1080).

INTERNET_INVALID_PORT_NUMBER

Uses the default port for the service specified by dwService.

ldwService
ValueMeaning

INTERNET_SERVICE_FTP

FTP service.

INTERNET_SERVICE_GOPHER

Gopher service.

INTERNET_SERVICE_HTTP

HTTP service.

(2)建立请求

调用 HttpOpenRequest 来建立一个 HTTP 请求,不过这个函数不会自动把请求发送出去,要发送请求需要调用 HttpSendRequest

HttpOpenRequest 原型


HINTERNET HttpOpenRequest(
__in HINTERNET hConnect// InternetConnect 函数返回的句柄
__in LPCTSTR lpszVerb// 动作,有GET, PUT, POST。也可以设置为 NULL ,会被当成默认的 GET 来用
__in LPCTSTR lpszObjectName// 一个描述你请求资源的字符串,当请求一个默认页面时令这个参数指向一个空串
__in LPCTSTR lpszVersion// HTTP 版本,这个参数为 NULL 时,默认使用""HTTP/1.1""
__in LPCTSTR lpszReferer// 说明了lpszObjectName是取自哪个文件,可以设为NULL
__in LPCTSTR *lplpszAcceptTypes//
  是一个指向 LPCTSTR数组的指针!数组以一个NULL指针结束。 指定了程序接受的内容的类型,设为空则不接受 任何类型的内容,设为空串则等价于""text/*"",即不接受文本文件以外的图片等文件,只接受某种特定的文件可以用类似"image/gif, image/jpeg"的方式。关于更多内容类型 请看这里
__in DWORD dwFlags// 一般都可以设置为 0
__in DWORD_PTR dwContext // 一般都可以设置为 0
);
(3)添加请求

HttpAddRequestHeaders 原型

BOOL HttpAddRequestHeaders(
   __in HINTERNET hConnect,//HttpOpenRequest 返回的句柄
   __in LPCTSTR lpszHeaders,//包含要添加到请求中的头的字符串的指针,每个头都要以一个 CR/LF ( "r"n"r"n  ) 对结束
   __in DWORD dwHeadersLength,//lpszHeaders指向的字符串的长度(以TCHAR类型记). 如果这个参数被设为-1,则字符串被当作以0结尾的字符串处理,自动计算该字符串的长度
   __in DWORD dwModifiers
);
dwModifiers

可以是下面这些值的组合

ValueMeaning

HTTP_ADDREQ_FLAG_ADD

Adds the header if it does not exist. Used with HTTP_ADDREQ_FLAG_REPLACE.

HTTP_ADDREQ_FLAG_ADD_IF_NEW

Adds the header only if it does not already exist; otherwise, an error is returned.

HTTP_ADDREQ_FLAG_COALESCE

Coalesces headers of the same name.

HTTP_ADDREQ_FLAG_COALESCE_WITH_COMMA

Coalesces headers of the same name. For example, adding "Accept: text/*" followed by "Accept: audio/*" with this flag results in the formation of the single header "Accept: text/*, audio/*". This causes the first header found to be coalesced. It is up to the calling application to ensure a cohesive scheme with respect to coalesced/separate headers.

HTTP_ADDREQ_FLAG_COALESCE_WITH_SEMICOLON

Coalesces headers of the same name using a semicolon.

HTTP_ADDREQ_FLAG_REPLACE

Replaces or removes a header. If the header value is empty and the header is found, it is removed. If not empty, the header value is replaced.

(4)发送一个请求

HttpSendRequest

BOOL HttpSendRequest(
__in HINTERNET hRequest, //HttpOpenRequst 返回的句柄
__in LPCTSTR lpszHeaders, //附加到请求上的头,可以为NULL
__in DWORD dwHeadersLength, //lpszHeaders指向的字符串的长度(以TCHAR类型记). 如果这个参数被设为-1,当调用的是HttpSendRequestA时则字符串被当作以0结尾的字符串处理,自动计算该字符串的长度。当调用的是 HttpSendRequestW时就会产生一个错误
__in LPVOID lpOptional, //当使用POST或PUT方法时,这个参数指向的数据会紧接着请求被发送出去。没有需要发送的数据则可以设置为NULL
__in DWORD dwOptionalLength //lpOptional数据的字节长度,无数据时设置为0
);
(5)向服务器发送数据

方法一:参见上面HttpSentRequest的lpOptional参数的说明

方法二:使用 InternetWriteFile 向一个句柄里发送数据,然后使用 HttpSendRequestEx 发送

(6)查询一个请求的信息

HttpQueryInfo

BOOL HttpQueryInfo(
__in      HINTERNET hRequest, /*由 HttpOpenRequest 或 InternetOpenUrl 返回的句柄*/
__in      DWORD dwInfoLevel,/*
  Query Info Flags. */
   __inout LPVOID lpvBuffer, /*用于存储查询结果的缓冲区,不可为NULL*/
__inout LPDWORD lpdwBufferLength,/*lpvBuffer指向的缓冲区的字节长度
         若函数执行成功,这个变量存储的是写到缓冲区里的数据长度。如果数据是字符串则返回的长度不包括字符串的结束字符
         如果函数发生 ERROR_INSUFFICIENT_BUFFER 错误, 则这个变量里保存的是数据的实际字节长度程序需要根据这个长度重新分配内存,再执行一次这个函数*/
__inout LPDWORD lpdwIndex /*没看大明白,不过似乎可以设置为NULL
        Pointer to a zero-based header index used to enumerate multiple headers with the same name.
        When calling the function, this parameter is the index of the specified header to return.
        When the function returns, this parameter is the index of the next header.
        If the next index cannot be found, ERROR_HTTP_HEADER_NOT_FOUND is returned.*/
);
(7) WWW 上下载资源

在调用 HttpOpenRequest 和 HttpSendRequest 之后, 程序可以使用 InternetReadFileInternetQueryDataAvailable,InternetSetFilePointer 来下载HTTP服务器上的资源了。

BOOL InternetQueryDataAvailable(//查询数据的长度

__in   HINTERNET hFile, //由 InternetOpenUrl, FtpOpenFile, GopherOpenFile, 或 HttpOpenRequest

返回的句柄

__out  LPDWORD lpdwNumberOfBytesAvailable, //用于存放数据长度的指针

__in   DWORD dwFlags, //保留参数,置0

__in   DWORD_PTR dwContext //保留参数,置0

); 
BOOL InternetReadFile( //读取句柄的数据
__in    HINTERNET hFile, // InternetOpenUrl, FtpOpenFile, GopherOpenFile, HttpOpenRequest 创建的句柄
__out LPVOID lpBuffer, // 存放数据的缓冲区
__in    DWORD dwNumberOfBytesToRead, // 准备读取的字节数
__out LPDWORD lpdwNumberOfBytesRead // 读取了的字节数
);

DWORD InternetSetFilePointer( //设置InternetReadFile的文件位置(莫非多线程下载就是用这个实现的?),服务器不支持随机访问的话函数调用会失败,如果 InternetReadFile已经读取到了文件的末尾,这个函数的调用也会失败。
__in HINTERNET hFile, //由 InternetOpenUrl (on an HTTP or HTTPS URL)创建。 或由HttpOpenRequest 创建(使用 GET 或 HEAD方法,而且句柄已经被 HttpSendRequest 访问过了). 
这个句柄也不可以使用 INTERNET_FLAG_DONT_CACHE 或 INTERNET_FLAG_NO_CACHE_WRITE 标志
__in LONG lDistanceToMove, //移动的字节数。正数向后移动,负数向前移动
__in PVOID pReserved, //保留参数,为NULL
__in DWORD dwMoveMethod, //指定了移动指针时的参考点。FILE_BEGIN(使用这个标志时,移动的字节数被当作无符号数处理)、FILE_CURRENT、FILE_END(如果内容的长度无法获得,则使用这个标志时会失败)
__in DWORD_PTR dwContext //保留参数,为NULL

);

///

SocketAPI,CAsyncSocket,CSocket内幕及其用法

Socket有同步阻塞方式和异步非阻塞方式两种使用,事实上同步和异步在我们编程的生涯中可能遇到了很多,而Socket也没什么特别。虽然同步好用,不费劲,但不能满足一些应用场合,其效率也很低。
    也许初涉编程的人不能理解“同步(或阻塞)”和“异步(或非阻塞)”,其实简单两句话就能讲清楚,同步和异步往往都是针对一个函数来说的,“同步”就是函数直到其要执行的功能全部完成时才返回,而“异步”则是,函数仅仅做一些简单的工作,然后马上返回,而它所要实现的功能留给别的线程或者函数去完成。例如,SendMessage就是“同步”函数,它不但发送消息到消息队列,还需要等待消息被执行完才返回;相反PostMessage就是个异步函数,它只管发送一个消息,而不管这个消息是否被处理,就马上返回。

一、Socket API
    首先应该知道,有Socket1.1提供的原始API函数,和Socket2.0提供的一组扩展函数,两套函数。这两套函数有重复,但是2.0提供的函数功能更强大,函数数量也更多。这两套函数可以灵活混用,分别包含在头文件Winsock.h,Winsock2.h,分别需要引入库 wsock32.lib、Ws2_32.lib。
    1、默认用作同步阻塞方式,那就是当你从不调用WSAIoctl()和ioctlsocket()来改变Socket IO模式,也从不调用WSAAsyncSelect()和WSAEventSelect()来选择需要处理的Socket事件。正是由于函数accept (),WSAAccept(),connect(),WSAConnect(),send(),WSASend(),recv(),WSARecv()等函数被用作阻塞方式,所以可能你需要放在专门的线程里,这样以不影响主程序的运行和主窗口的刷新。
    2、如果作为异步用,那么程序主要就是要处理事件。它有两种处理事件的办法:
    第一种,它常关联一个窗口,也就是异步Socket的事件将作为消息发往该窗口,这是由WinSock扩展规范里的一个函数WSAAsyncSelect()来实现和窗口关联。最终你只需要处理窗口消息,来收发数据。
   第二种,用到了扩展规范里另一个关于事件的函数WSAEventSelect(),它是用事件对象的方式来处理Socket事件,也就是,你必须首先用 WSACreateEvent()来创建一个事件对象,然后调用WSAEventSelect()来使得Socket的事件和这个事件对象关联。最终你将要在一个线程里用WSAWaitForMultipleEvents()来等待这个事件对象被触发。这个过程也稍显复杂。
二、CAsyncSocket
    看类名就知道,它是一个异步非阻塞Socket封装类,CAsyncSocket::Create()有一个参数指明了你想要处理哪些Socket事件,你关心的事件被指定以后,这个Socket默认就被用作了异步方式。那么CAsyncSocket内部到底是如何将事件交给你的呢?
    CAsyncSocket的Create()函数,除了创建了一个SOCKET以外,还创建了个CSocketWnd窗口对象,并使用 WSAAsyncSelect()将这个SOCKET与该窗口对象关联,以让该窗口对象处理来自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是简单地回调CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(), CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虚函数。所以CAsyncSocket的派生类,只需要在这些虚函数里添加发送和接收的代码。
简化后,大致的代码为:

bool CAsyncSocket::Create( long lEvent ) file://参数lEvent是指定你所关心的Socket事件
    {
     m_hSocket = socket( PF_INET, SOCK_STREAM, 0 ); file://创建Socket本身
     CSocketWnd* pSockWnd = new CSocketWnd; file://创建响应事件的窗口,实际的这个窗口在AfxSockInit()调用时就被创建了。
     pSockWnd->Create(...);
     WSAAsyncSelect( m_hSocket, pSockWnd->m_hWnd, WM_SOCKET_NOTIFY, lEvent ); file://Socket事件和窗口关联
    }

    static void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
    {
     CAsyncSocket Socket;
     Socket.Attach( (SOCKET)wParam ); file://wParam就是触发这个事件的Socket的句柄
     int nErrorCode = WSAGETSELECTERROR(lParam); file://lParam是错误码与事件码的合成
     switch (WSAGETSELECTEVENT(lParam))
     {
     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;
     }
    }
    CSocketWnd类大致为:
    BEGIN_MESSAGE_MAP(CSocketWnd, CWnd)
     ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify)
    END_MESSAGE_MAP()
    LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
    {
     CAsyncSocket::DoCallBack( wParam, lParam ); file://收到Socket事件消息,回调CAsyncSocket的DoCallBack()函数
     return 0L;
    }


    然而,最不容易被初学Socket编程的人理解的,也是本文最要提醒的一点是,客户方在使用CAsyncSocket::Connect() 时,往往返回一个WSAEWOULDBLOCK的错误(其它的某些函数调用也如此),实际上这不应该算作一个错误,它是Socket提醒我们,由于你使用了非阻塞Socket方式,所以(连接)操作需要时间,不能瞬间建立。既然如此,我们可以等待呀,等它连接成功为止,于是许多程序员就在调用 Connect()之后,Sleep(0),然后不停地用WSAGetLastError()或者CAsyncSocket::GetLastError ()查看Socket返回的错误,直到返回成功为止。这是一种错误的做法,断言,你不能达到预期目的。事实上,我们可以在Connect()调用之后等待 CAsyncSocket::OnConnect()事件被触发,CAsyncSocket::OnConnect()是要表明Socket要么连接成功了,要么连接彻底失败了。至此,我们在CAsyncSocket::OnConnect()被调用之后就知道是否Socket连接成功了,还是失败了。
    类似的,Send()如果返回WSAEWOULDBLOCK错误,我们在OnSend()处等待,Receive()如果返回WSAEWOULDBLOCK错误,我们在OnReceive()处等待,以此类推。
    还有一点,也许是个难点,那就是在客户方调用Connect()连接服务方,那么服务方如何Accept(),以建立连接的问题。简单的做法就是在监听的Socket收到OnAccept()时,用一个新的CAsyncSocket对象去建立连接,例如:
void CMySocket::OnAccept( int ErrCode )
{
       CMySocket* pSocket = new CMySocket;
       Accept( *pSocket );
}
    于是,上面的pSocket和客户方建立了连接,以后的通信就是这个pSocket对象去和客户方进行,而监听的Socket仍然继续在监听,一旦又有一个客户方要连接服务方,则上面的OnAccept()又会被调用一次。当然pSocket是和客户方通信的服务方,它不会触发OnAccept()事件,因为它不是监听Socket。
三、CSocket
   CSocket是MFC在CAsyncSocket基础上派生的一个同步阻塞Socket的封装类。它是如何又把CAsyncSocket变成同步的,而且还能响应同样的Socket事件呢?
   其实很简单,CSocket在Connect()返回WSAEWOULDBLOCK错误时,不是在OnConnect(),OnReceive()这些事件终端函数里去等待。你先必须明白Socket事件是如何到达这些事件函数里的。这些事件处理函数是靠CSocketWnd窗口对象回调的,而窗口对象收到来自Socket的事件,又是靠线程消息队列分发过来的。总之,Socket事件首先是作为一个消息发给CSocketWnd窗口对象,这个消息肯定需要经过线程消息队列的分发,最终CSocketWnd窗口对象收到这些消息就调用相应的回调函数(OnConnect()等)。
   所以,CSocket在调用Connect()之后,如果返回一个WSAEWOULDBLOCK错误时,它马上进入一个消息循环,就是从当前线程的消息队列里取关心的消息,如果取到了WM_PAINT消息,则刷新窗口,如果取到的是Socket发来的消息,则根据Socket是否有操作错误码,调用相应的回调函数(OnConnect()等)。
   大致的简化代码为: 
BOOL CSocket::Connect( ... )
    {
     if( !CAsyncSocket::Connect( ... ) )
     {
        if( WSAGetLastError() == WSAEWOULDBLOCK ) file://由于异步操作需要时间,不能立即完成,所以Socket返回这个错误
        {
         file://进入消息循环,以从线程消息队列里查看FD_CONNECT消息,直到收到FD_CONNECT消息,认为连接成功。
         while( PumpMessages( FD_CONNECT ) );
        }
     }
    }
    BOOL CSocket::PumpMessages( UINT uEvent )
    {
            CWinThread* pThread = AfxGetThread();
            while( bBlocking ) file://bBlocking仅仅是一个标志,看用户是否取消对Connect()的调用
            {
                    MSG msg;
                    if( PeekMessage( &msg, WM_SOCKET_NOTIFY ) )
                    {
                         if( msg.message == WM_SOCKET_NOTIFY && WSAGETSELECTEVENT(msg.lParam) == uStopFlag )
                         {
                                 CAsyncSocket::DoCallBack( msg.wParam, msg.lParam );
                                 return TRUE;
                         }        
                 }
                 else
                {
                         OnMessagePending(); file://处理消息队列里的其它消息
                         pThread->OnIdle(-1);
                }
         }
    }
    BOOL CSocket::OnMessagePending()
    {
            MSG msg;
             if( PeekMessage( &msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE ) )
             { file://这里仅关心WM_PAINT消息,以处理阻塞期间的主窗口重画
                     ::DispatchMessage( &msg );
                     return FALSE;
             }
             return FALSE;
    }

   其它的CSocket函数,诸如Send(),Receive(),Accept()都在收到WSAEWOULDBLOCK错误时,进入 PumpMessages()消息循环,这样一个原本异步的CAsyncSocket,到了派生类CSocket,就变成同步的了。
明白之后,我们可以对CSocket应用自如了。比如有些程序员将CSocket的操作放入一个线程,以实现多线程的异步Socket(通常,同步+多线程 相似于 异步 )。
四、CSocketFile
另外,进行Socket编程,不能不提到CSocketFile类,其实它并不是用来在Socket双方发送文件的,而是将需要序列化的数据,比如一些结构体数据,传给对方,这样,程序的CDocument()的序列化函数就完全可以和CSocketFile 联系起来。例如你有一个CMyDocument实现了Serialize(),你可以这样来将你的文档数据传给Socket的另一方:
CSocketFile file( pSocket );
CArchive ar( &file, CArchive::store );
pDocument->Serialize( ar );
ar.Close();
同样,接收一方可以只改变上面的代码为CArchive ar( &file, CArchive::load );即可。
   注意到,CSocketFile类虽然从CFile派生,但它屏蔽掉了CFile::Open()等函数,而函数里仅扔出一个例外。那么也就是说,你不能调用CSocketFile的Open函数来打开一个实实在在的文件,否则会导致例外,如果你需要利用CSocketFile来传送文件,你必须提供 CSocketFile类的这些函数的实现。
    再一点,CArchive不支持在datagram的Socket连接上序列化数据。


    找了好些文章,感觉这篇挺容易懂的。原本WinSock API是同步模式,如果网络出了问题或者其它原因他会一直阻塞,原因就是太过简单的同步模式,它是让像send()这样的调用一直处于线程挂起状态,英特网上的通信可不像在PC里CPU跟外设的通信一样简单,比如我们打开一个网页,不可能一下就打开,阻塞是常有的事,很多时候我们都在等,等待你的PC连到外面集线器的那根线上什么时候有信号过来,事实是这样,需要等待,但是我们不能让线程傻傻地等待,采用多线程的方法,让另外一个线程等待,设定超时计时器等等方法,让原来的线程可以干别的事,比如写一行字‘正在连接...’,这就是异步方式,CAsyncSocket就是个比较好的异步方式封装。

//

CAsyncSocket

这几天都在研么MFC的套接字类CAsyncSocket的用法, 将一些心得和实践中遇到的问题总结一下。

 

一、        一些网络的基本概念

1.       同步:指的是发送方不等接收方响应,便接着发下个数据包的通信方式;

2.       异步:指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方式

3.       阻塞:指调用某函数时,直到该函数完成操作,才返回;否则一直阻塞在该调用上

4.       非阻塞:指调用某操作时,不管操作是否成功都立即返回,而不会挂在该操作上

 

CAsyncSocket属于异步非阻塞类;

CSocket是MFC在CAsyncSocket基础上派生的一个同步阻塞Socket的封装类

二、       CAsyncSocket的使用(伪码)

服务器端:

m_pListSocket   =    new    CAsyncSocket(); 
m_pListSocket-> Create( 端口,地址);                    //  创建

m_pListSocket->Listen();                             //  开始监听

 

m_pListSocket::OnAccept( )                        //  有客户端请求连接时响应

{

m_pSocket   =    new    CAsyncSocket(); 
m_pListSocket-> Accept(m_pSocket);        //  建立通信,成功后m_pSocket就用于发送和接受.

 

//  m_pSocket 就相当于连接的那个客户端了

}

 

m_pSocket::OnRecive(  int nErrorCode)

{

          if( nErrorCode == 0)

{

          Recevie();                             //  接受客户端发送来的信息

}

          CAsyncSocket::OnRecive( nErrorCode)

}

 

m_pSocket::OnSend()

{

          Send();                         //  发送信息,该事件触发条件见下节

}

 

m_pListSocket->Close();

 

delete m_pListSocket;

delete m_pSocket;

客户端:

m_pClientSocket   =    new    CAsyncSocket(); 
m_pClientSocket -> Create( 端口,地址);                       //  创建

m_pClientSocket->Connect();                                          //  连接服务器,最终将触发服务器的OnAccept();

m_ pClientSocket::OnConnect()                                             //  当连接上服务器

{

         

}

 

m_ pClientSocket::OnRecive(  int nErrorCode)

{

          if( nErrorCode == 0)

{

          Recevie();                             //  接受客户端发送来的信息

}

          CAsyncSocket::OnRecive( nErrorCode)

}

 

m_pClientSocket::OnSend(  int nErrorCode)

{

          Send();                                   //  发送信息

}

 

m_pClientSocket->Close();

三、       CAsyncSocket异步机制

由于CAsyncSocket采用的是异步非阻塞机制,所以你随时可以发包,也随时可能收到包。

发送、接收函数都是异步非阻塞的,顷刻就能完成,所以收发交错进行着。也正因为如此,仅调用它们并不能保障发送或接收的完成。

 

例如发送函数Send,调用它可能有3种结果:错误、部分完成、全部完成。其中错误又分两种情况:一种是由各种网络问题导致的失败,你需要马上决定是放弃本次操作,还是启用某种对策;另一种是“忙”,你实际上不用马上理睬。你需要调用GetLastError来判断是哪种情况,GetLastError返回WSAEWOULDBLOCK,代表“忙”,为什么当你Send得
到WSAEWOULDBLOCK却不用理睬呢?因为CAsyncSocket会记得你的SendWSAEWOULDBLOCK了,待发送的数据会写入CAsyncSocket内部的发送缓冲区,并会在不忙的时候自动调用OnSend,发送内部缓冲区里的数据。同样,如果Send只完成了一部分,你也不需要理睬,尚未发送的数据同样会写入CAsyncSocket内部的发送缓冲区,并在不“忙”的时候自动调用OnSend完成发送。

与OnSend协助Send完成工作一样,OnRecieve、OnConnect、OnAccept也会分别协助Recieve、Connect、Accept完成工作。这一切都通过消息机制完成。

 

在你使用CAsyncSocket之前,必须调用AfxSocketInit初始化WinSock环境,而AfxSocketInit会创建一个隐藏的CSocketWnd对象,由于这个对象由Cwnd派生,因此它能够接收Windows消息。一方面它会接受各个CAsyncSocket的状态报告,另一方面它能捕捉系统发出的各种SOCKET事件。所以它能够成为高层CAsyncSocket对象与WinSock底层之间的桥梁:例如某CAsyncSocketSendWSAEWOULDBLOCK了,它就会发送一条消息给CSocketWnd作为报告,CSocketWnd会维护一个报告登记表,当它收到底层WinSock发出的空闲消息时,就会检索报告登记表,然后直接调用报告者的OnSend函数。所以前文所说的CAsyncSocket会自动调用OnXxx,实际上是不对的,真正的调用者是CSocketWnd——它是一个CWnd对象,运行在独立的线程中。

 

四、       网络事件处理流程

 

在理解了上面的机制后, 让我们了解下CAsyncSocket的通信流程

 

 

 OnSend,除了在对方发送消息来的时候响应外,还会在缓冲区有空闲的时候自动触发;

如果每次发送的数据比较简单,不会造成WASEWOULDBLOCK(阻塞),不会触发OnSend;

因此小数据直接Send就行了,大数据就需要在OnSend判断数据发送是否正确;

 

如何手动触发OnSend()呢,采用AsyncSelect( FD_WRITE),通知CsocketWnd窗口处理写

数据操作; 同样AsyncSelect(FD_READ)将通知CsocketWnd窗口当有消息传来的时候触发OnRecevie();

 

   BOOL AsyncSelect( long lEvent = FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE );     // 请求Socket响应以上事件

 

五、       消息为何只接收一次

编程中遇到这个问题,发现很多人都遇到过这个问题。

症状如下:Socket连接后只能发送一次消息,发送第二次消息的时候,另一方就接收不到;

原因是:没有让Socket改变响应事件的发式

解决方法:在OnReceive()中,Receive()后调用AsyncSelect(FD_READ);

 

 

 

Void CMyAsyncSocket::OnReceive( int nErrorCode)

{

         Receive();

         AsyncSelect(FD_READ);

}

 

或则 调用父类的OnReceive()

 

Void CMyAsyncSocket::OnReceive(  int nErrorCode)

{

         Receive();

 

         CAsyncSocket::OnReceive( nErrorCode);

}


 

六、   为何服务器Socket不监听

在创建服务器Socket的时候,只有采用SOCK_STREAM(字符流),Listen才能成功;

采用SOCK_DGRAM(数据报文)创建的Socket是面向无连接发式(UDP),所以Listen不成功(有待验证)



CAsyncSocket和CSocket

CAsyncSocket
         看类名就知道,它是一个异步非阻塞Socket封装类,CAsyncSocket::Create()有一个参数指明了你想要处理哪些Socket事件,你关心的事件被指定以后,这个Socket默认就被用作了异步方式。那么CAsyncSocket内部到底是如何将事件交给你的呢?
         CAsyncSocket的Create()函数,除了创建了一个SOCKET以外,还创建了个CSocketWnd窗口对象,并使用WSAAsyncSelect()将这个SOCKET与该窗口对象关联,以让该窗口对象处理来自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是简单地回调CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(),CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虚函数。所以CAsyncSocket的派生类,只需要在这些虚函数里添加发送和接收的代码。
  
简化后,大致的代码为:
bool CAsyncSocket::Create( long lEvent )    //参数lEvent是指定你所关心的Socket事件
{
         m_hSocket = socket( PF_INET, SOCK_STREAM, 0 );   //创建Socket本身 
         CSocketWnd* pSockWnd = new CSocketWnd;   //创建响应事件的窗口,实际的这个窗口在AfxSockInit()调用时就被创建了。
         pSockWnd->Create(...); 
         WSAAsyncSelect( m_hSocket, pSockWnd->m_hWnd, WM_SOCKET_NOTIFY, lEvent ); //Socket事件和窗口关联
}
  
static void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
{
           CAsyncSocket Socket;
           Socket.Attach( (SOCKET)wParam ); //wParam就是触发这个事件的Socket的句柄
           int nErrorCode = WSAGETSELECTERROR(lParam); //lParam是错误码与事件码的合成
           switch (WSAGETSELECTEVENT(lParam))
           {
                 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;
            }
   }

   CSocketWnd类大致为:

   BEGIN_MESSAGE_MAP(CSocketWnd, CWnd)
        ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify)
   END_MESSAGE_MAP()

LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
{
         CAsyncSocket::DoCallBack( wParam, lParam ); //收到Socket事件消息,回调CAsyncSocket的DoCallBack()函数
         return 0L;
}

         然而,最不容易被初学Socket编程的人理解的,也是本文最要提醒的一点是,客户方在使用CAsyncSocket::Connect()时,往往返回一个WSAEWOULDBLOCK的错误(其它的某些函数调用也如此),实际上这不应该算作一个错误,它是Socket提醒我们,由于你使用了非阻塞Socket方式,所以(连接)操作需要时间,不能瞬间建立。既然如此,我们可以等待呀,等它连接成功为止,于是许多程序员就在调用Connect()之后,Sleep(0),然后不停地用WSAGetLastError()或者CAsyncSocket::GetLastError()查看Socket返回的错误,直到返回成功为止。这是一种错误的做法,断言,你不能达到预期目的。事实上,我们可以在Connect()调用之后等待CAsyncSocket::OnConnect()事件被触发,CAsyncSocket::OnConnect()是要表明Socket要么连接成功了,要么连接彻底失败了。至此,我们在CAsyncSocket::OnConnect()被调用之后就知道是否Socket连接成功了,还是失败了。
         类似的,Send()如果返回WSAEWOULDBLOCK错误,我们在OnSend()处等待,Receive()如果返回WSAEWOULDBLOCK错误,我们在OnReceive()处等待,以此类推。
         还有一点,也许是个难点,那就是在客户方调用Connect()连接服务方,那么服务方如何Accept(),以建立连接的问题。简单的做法就是在监听的Socket收到OnAccept()时,用一个新的CAsyncSocket对象去建立连接,例如:

void CMySocket::OnAccept( int ErrCode )
{
        CMySocket* pSocket = new CMySocket;
        Accept( *pSocket );
}
         于是,上面的pSocket和客户方建立了连接,以后的通信就是这个pSocket对象去和客户方进行,而监听的Socket仍然继续在监听,一旦又有一个客户方要连接服务方,则上面的OnAccept()又会被调用一次。当然pSocket是和客户方通信的服务方,它不会触发OnAccept()事件,因为它不是监听Socket。

         CSocket
         CSocket是MFC在CAsyncSocket基础上派生的一个同步阻塞Socket的封装类。它是如何又把CAsyncSocket变成同步的,而且还能响应同样的Socket事件呢?
         其实很简单,CSocket在Connect()返回WSAEWOULDBLOCK错误时,不是在OnConnect(),OnReceive()这些事件终端函数里去等待。你先必须明白Socket事件是如何到达这些事件函数里的。这些事件处理函数是靠CSocketWnd窗口对象回调的,而窗口对象收到来自Socket的事件,又是靠线程消息队列分发过来的。总之,Socket事件首先是作为一个消息发给CSocketWnd窗口对象,这个消息肯定需要经过线程消息队列的分发,最终CSocketWnd窗口对象收到这些消息就调用相应的回调函数(OnConnect()等)。
         所以,CSocket在调用Connect()之后,如果返回一个WSAEWOULDBLOCK错误时,它马上进入一个消息循环,就是从当前线程的消息队列里取关心的消息,如果取到了WM_PAINT消息,则刷新窗口,如果取到的是Socket发来的消息,则根据Socket是否有操作错误码,调用相应的回调函数(OnConnect()等)。
         大致的简化代码为:

   BOOL CSocket::Connect( ... )
   {
        if( !CAsyncSocket::Connect( ... ) )
        {
                if( WSAGetLastError() == WSAEWOULDBLOCK ) //由于异步操作需要时间,不能立即完成,所以Socket返回这个错误
                {
                      //进入消息循环,以从线程消息队列里查看FD_CONNECT消息,直到收到FD_CONNECT消息,认为连接成功。
                      while( PumpMessages( FD_CONNECT ) );
                 }
        }
   }
   BOOL CSocket::PumpMessages( UINT uEvent )
   {
           CWinThread* pThread = AfxGetThread();
           while( bBlocking ) //bBlocking仅仅是一个标志,看用户是否取消对Connect()的调用
           {
                   MSG msg;
                   if( PeekMessage( &msg, WM_SOCKET_NOTIFY ) )
                   {
                          if( msg.message == WM_SOCKET_NOTIFY && WSAGETSELECTEVENT(msg.lParam) == uStopFlag )
                          {
                                  CAsyncSocket::DoCallBack( msg.wParam, msg.lParam );
                                  return TRUE;
                          }     
                  }
                  else
                 {
                          OnMessagePending(); //处理消息队列里的其它消息
                          pThread->OnIdle(-1);
                 }
          }
   }
   BOOL CSocket::OnMessagePending()
   {
            MSG msg;
            if( PeekMessage( &msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE ) )
            { //这里仅关心WM_PAINT消息,以处理阻塞期间的主窗口重画
                ::DispatchMessage( &msg );
                return FALSE;
            }
            return FALSE;
   }

         其它的CSocket函数,诸如Send(),Receive(),Accept()都在收到WSAEWOULDBLOCK错误时,进入PumpMessages()消息循环,这样一个原本异步的CAsyncSocket,到了派生类CSocket,就变成同步的了。
         明白之后,我们可以对CSocket应用自如了。比如有些程序员将CSocket的操作放入一个线程,以实现多线程的异步Socket(通常,同步+多线程 相似于 异步 )。
 

CAsyncSocket

CAsyncSocket
看类名就知道,它是一个异步非阻塞Socket封装类,CAsyncSocket::Create()有一个参数指明了你想要处理哪些Socket事件,你关心的事件被指定以后,这个Socket默认就被用作了异步方式。那么CAsyncSocket内部到底是如何将事件交给你的呢?
    CAsyncSocket的Create()函数,除了创建了一个SOCKET以外,还创建了个CSocketWnd窗口对象,并使用WSAAsyncSelect()将这个SOCKET与该窗口对象关联,以让该窗口对象处理来自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是简单地回调CAsyncSocket::OnReceive(),CAsyncSocket::OnSend(),CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虚函数。所以CAsyncSocket的派生类,只需要在这些虚函数里添加发送和接收的代码。
  
  简化后,大致的代码为:
  bool CAsyncSocket::Create( long lEvent ) file://参/数lEvent是指定你所关心的Socket事件
  {
   m_hSocket = socket( PF_INET, SOCK_STREAM, 0 ); file://创/建Socket本身

   CSocketWnd* pSockWnd = new CSocketWnd; file://创/建响应事件的窗口,实际的这个窗口在AfxSockInit()调用时就被创建了。
   pSockWnd->Create(...);

   WSAAsyncSelect( m_hSocket, pSockWnd->m_hWnd, WM_SOCKET_NOTIFY, lEvent ); file://Socket/事件和窗口关联
  }
  
  static void PASCAL CAsyncSocket::DoCallBack(WPARAM wParam, LPARAM lParam)
  {
   CAsyncSocket Socket;
   Socket.Attach( (SOCKET)wParam ); 
file://wParam/就是触发这个事件的Socket的句柄
   int nErrorCode = WSAGETSELECTERROR(lParam); 
file://lParam/是错误码与事件码的合成
   switch (WSAGETSELECTEVENT(lParam))
   {
   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;
   }
  }

  CSocketWnd类大致为:

  BEGIN_MESSAGE_MAP(CSocketWnd, CWnd)
   ON_MESSAGE(WM_SOCKET_NOTIFY, OnSocketNotify)
  END_MESSAGE_MAP()

  LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam)
  {
   CAsyncSocket::DoCallBack( wParam, lParam ); file://收/到Socket事件消息,回调CAsyncSocket的DoCallBack()函数
   return 0L;
  }

  然而,最不容易被初学Socket编程的人理解的,也是本文最要提醒的一点是,客户方在使用CAsyncSocket::Connect()时,往往返回一个WSAEWOULDBLOCK的错误(其它的某些函数调用也如此),实际上这不应该算作一个错误,它是Socket提醒我们,由于你使用了非阻塞Socket方式,所以(连接)操作需要时间,不能瞬间建立。既然如此,我们可以等待呀,等它连接成功为止,于是许多程序员就在调用Connect()之后,Sleep(0),然后不停地用WSAGetLastError()或者CAsyncSocket::GetLastError()查看Socket返回的错误,直到返回成功为止。这是一种错误的做法,断言,你不能达到预期目的。事实上,我们可以在Connect()调用之后等待CAsyncSocket::OnConnect()事件被触发,CAsyncSocket::OnConnect()是要表明Socket要么连接成功了,要么连接彻底失败了。至此,我们在CAsyncSocket::OnConnect()被调用之后就知道是否Socket连接成功了,还是失败了。
  类似的,Send()如果返回WSAEWOULDBLOCK错误,我们在OnSend()处等待,Receive()如果返回WSAEWOULDBLOCK错误,我们在OnReceive()处等待,以此类推。
  还有一点,也许是个难点,那就是在客户方调用Connect()连接服务方,那么服务方如何Accept(),以建立连接的问题。简单的做法就是在监听的Socket收到OnAccept()时,用一个新的CAsyncSocket对象去建立连接,例如:

void CMySocket::OnAccept( int ErrCode )
{
       CMySocket* pSocket = new CMySocket;
       Accept( *pSocket );
}
    于是,上面的pSocket和客户方建立了连接,以后的通信就是这个pSocket对象去和客户方进行,而监听的Socket仍然继续在监听,一旦又有一个客户方要连接服务方,则上面的OnAccept()又会被调用一次。当然pSocket是和客户方通信的服务方,它不会触发OnAccept()事件,因为它不是监听Socket。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值