Socket同步 异步通讯

MFC对SOCKET编程的支持实际上是很充分的,然而其文档是语焉不详的。以致于大多数用VC编写的功能稍
复杂的网络程序,仍是使用API的。故CAsyncSocket及CSocket事实上成为疑难,群众多敬而远之。余
好事者也,不忍资源浪费,特为之注解。

1、CAsyncSocket与CSocket的区别

前者是异步通讯,后者是同步通讯;前者是非阻塞模式,后者是阻塞模式。另外,异步非阻塞模式有
时也被称为长链接,同步阻塞模式则被称为短链接。为了更明白地讲清楚二者的区别,举个例子:

设想你是一位体育老师,须要测验100位同窗的400米成绩。你固然不会让100位同窗一块儿起跑,由于当
同窗们返回终点时,你根原本不及掐表记录各位同窗的成绩。

若是你每次让一位同窗起跑并等待他回到终点你记下成绩后再让下一位起跑,直到全部同窗都跑完。恭
喜你,你已经掌握了同步阻塞模式。

你设计了一个函数,传入参数是学生号和起跑时间,返回值是到达终点的时间。你调用该函数100次,
就能完成此次测验任务。这个函数是同步的,由于只要你调用它,就能获得结果;这个函数也是阻塞的,
由于你一旦调用它,就必须等待,直到它给你结果,不能去干其余事情。

若是你一边每隔10秒让一位同窗起跑,直到全部同窗出发完毕;另外一边每有一个同窗回到终点就记录成
绩,直到全部同窗都跑完。恭喜你,你已经掌握了异步非阻塞模式。

你设计了两个函数,其中一个函数记录起跑时间和学生号,该函数你会主动调用100次;另外一个函数记
录到达时间和学生号,该函数是一个事件驱动的callback函数,当有同窗到达终点时,你会被动调用。
你主动调用的函数是异步的,由于你调用它,它并不会告诉你结果;这个函数也是非阻塞的,由于你一
旦调用它,它就立刻返回,你不用等待就能够再次调用它。但仅仅将这个函数调用100次,你并无完
成你的测验任务,你还须要被动等待调用另外一个函数100次。

固然,你立刻就会意识到,同步阻塞模式的效率明显低于异步非阻塞模式。那么,谁还会使用同步阻塞
模式呢?

不错,异步模式效率高,但更麻烦,你一边要记录起跑同窗的数据,一边要记录到达同窗的数据,并且
同窗们回到终点的次序与起跑的次序并不相同,因此你还要不停地在你的成绩册上查找学生号。忙乱之
中你每每会张冠李戴。

你可能会想出更聪明的办法:你带了不少块秒表,让同窗们分组互相测验。恭喜你!你已经掌握了多线
程同步模式!

每一个拿秒表的同窗均可以独立调用你的同步函数,这样既不容易出错,效率也大大提升,只要秒表足够
多,同步的效率也能达到甚至超过异步。

能够理解,你现的问题多是:既然多线程同步既快又好,异步模式还有存在的必要吗?

很遗憾,异步模式依然很是重要,由于在不少状况下,你拿不出不少秒表。你须要通讯的对端系统可能
只容许你创建一个SOCKET链接,不少金融、电信行业的大型业务系统都如此要求。

如今,你应该已经明白了:CAsyncSocket用于在少许链接时,处理大批量无步骤依赖性的业务。CSocket
用于处理步骤依赖性业务,或在可多链接时配合多线程使用。


2、CAsyncSocket异步机制

当你得到了一个异步链接后,实际上你扫除了发送动做与接收动做之间的依赖性。因此你随时能够发包,
也随时可能收到包。发送、接收函数都是异步非阻塞的,顷刻就能返回,因此收发交错进行着,你能够
一直工做,保持很高的效率。可是,正由于发送、接收函数都是异步非阻塞的,因此仅调用它们并不能
保障发送或接收的完成。例如发送函数Send,调用它可能有4种结果:

1、错误,Send()==SOCKET_ERROR,GetLastError()!=WSAEWOULDBLOCK,这种状况可能由各类网络问题导
致,你须要立刻决定是放弃本次操做,仍是启用某种对策

2、忙,Send()==SOCKET_ERROR,GetLastError()==WSAEWOULDBLOCK,致使这种状况的缘由是,你的发送
缓冲区已被填满或对方的接受缓冲区已被填满。这种状况你实际上不用立刻理睬。由于CAsyncSocket会
记得你的Send WSAEWOULDBLOCK了,待发送的数据会写入CAsyncSocket内部的发送缓冲区,并会在不忙的
时候自动调用OnSend,发送内部缓冲区里的数据。

3、部分完成,0<Send(pBuf,nLen)<nLen,致使这种状况的缘由是,你的发送缓冲区或对方的接收缓冲区
中剩余的空位不足以容纳你此次须要发送的所有数据。处理这种状况的一般作法是继续发送还没有发送的
数据直到所有完成或WSAEWOULDBLOCK。这种状况很容易让人产生疑惑,既然缓冲区空位不足,那么本次
发送就已经填满了缓冲区,干吗还要继续发送呢,就像WSAEWOULDBLOCK了同样直接交给OnSend去处理剩
余数据的发送不是更合理吗?然而很遗憾,CAsyncSocket不会记得你只完成了部分发送任务从而在合适
的时候触发OnSend,由于你并无WSAEWOULDBLOCK。你可能认为既然已经填满缓冲区,继续发送必然会
WSAEWOULDBLOCK,其实否则,假如WSAEWOULDBLOCK是因为对方读取接收缓冲区不及时引发的,继续发送
的确极可能会WSAEWOULDBLOCK,但假如WSAEWOULDBLOCK是因为发送缓冲区被填满,就不必定了,由于你
的网卡处理发送缓冲区中数据的速度不见得比你往发送缓冲区拷贝数据的速度更慢,这要取决与你竞争
CPU、内存、带宽资源的其余应用程序的具体状况。假如这时候CPU负载较大而网卡负载较低,则虽然刚
刚发送缓冲区是满的,你继续发送也不会WSAEWOULDBLOCK。

4、完成,Send(pBuf,nLen)==nLen

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

在你使用CAsyncSocket以前,必须调用AfxSocketInit初始化WinSock环境,而AfxSocketInit会建立一个
隐藏的CSocketWnd对象,因为这个对象由Cwnd派生,所以它可以接收Windows消息。因此它可以成为高层
CAsyncSocket对象与WinSock底层之间的桥梁。例如某CAsyncSocket在Send时WSAEWOULDBLOCK了,它就会
发送一条消息给CSocketWnd做为报告,CSocketWnd会维护一个报告登记表,当它收到底层WinSock发出的
空闲消息时,就会检索报告登记表,而后直接调用报告者的OnSend函数。因此前文所说的CAsyncSocket会
自动调用OnXxx,其实是不对的,真正的调用者是CSocketWnd——它是一个CWnd对象,运行在独立的线
程中。

使用CAsyncSocket时,Send流程和Recieve流程是不一样的,不理解这一点就不可能顺利使用CAsyncSocket。

MSDN对CAsyncSocket的解释很容易让你理解为:只有OnSend被触发时你Send才有意义,你才应该Send,
一样只有OnRecieve被触发时你才应该Recieve。很不幸,你错了:

你会发现,链接创建的同时,OnSend就第一次被触发了,嗯,这很好,但你如今还不想Send,你让OnSend
返回,干点其余的事情,等待下一次OnSend试试看?实际上,你再也等不到OnSend被触发了。由于,除
了第一次之外,OnSend的任何一次触发,都源于你调用了Send,但碰到了WSAEWOULDBLOCK!

因此,使用CAsyncSocket时,针对发送的流程逻辑应该是:你需两个成员变量,一个发送任务表,一个
记录发送进度。你能够,也应该,在任何你须要的时候,主动调用Send来发送数据,同时更新任务表和
发送进度。而OnSend,则是你的负责擦屁股工做的助手,它被触发时要干的事情就是根据任务表和发送
进度调用Send继续发。若又没能将任务表所有发送完成,更新发送进度,退出,等待下一次OnSend;若
任务表已所有发送完毕,则清空任务表及发送进度。

使用CAsyncSocket的接收流程逻辑是不一样的:你永远不须要主动调用Recieve,你只应该在OnRecieve中等
待。因为你不可能知道将要抵达的数据类型及次序,因此你须要定义一个已收数据表做为成员变量来存储
已收到但还没有处理的数据。每次OnRecieve被触发,你只须要被动调用一次Recieve来接受固定长度的数据,
并添加到你的已收数据表后。而后你须要扫描已收数据表,若其中已包含一条或数条完整的可解析的业务
数据包,截取出来,调用业务处理窗口的处理函数来处理或做为消息参数发送给业务处理窗口。而已收数
据表中剩下的数据,将等待下次OnRecieve中被再次组合、扫描并处理。

在长链接应用中,链接可能由于各类缘由中断,因此你须要自动重连。你须要根据CAsyncSocket的成员变
量m_hSocket来判断当前链接状态:if(m_hSocket==INVALID_SOCKET)。固然,很奇怪的是,即便链接已经
中断,OnClose也已经被触发,你仍是须要在OnClose中主动调用Close,不然m_hSocket并不会被自动赋值
为INVALID_SOCKET。

在不少长链接应用中,除创建链接之外,还须要先Login,而后才能进行业务处理,链接并Login是一个步
骤依赖性过程,用异步方式处理反而会很麻烦,而CAsyncSocket是支持切换为同步模式的,你应该掌握在
适当的时候切换同异步模式的方法:

DWORD dw;

//切换为同步模式
dw=0;
IOCtl(FIONBIO,&dw);
...

//切换回异步模式
dw=1;
IOCtl(FIONBIO,&dw);


3、CSocket的用法

CSocket在CAsyncSocket的基础上,修改了Send、Recieve等成员函数,帮你内置了一个用以轮询收发缓冲区
的循环,变成了同步短链接模式。

短链接应用简单明了,CSocket常常不用派生就能够直接使用,但也有些问题:

1、用做监听的时候

曾经看到有人本身建立线程,在线程中建立CSocket对象进行Listen、Accept,若Accept成功则再起一个线
程继续Listen、Accept。。。能够说他彻底不理解CSocket,实际上CSocket的监听机制已经内置了多线程机
制,你只须要从CSocket派生,而后重载OnAccept:

//CListenSocket头文件
class CListenSocket : public CSocket
{
public:
    CListenSocket(HWND hWnd=NULL);
    HWND m_hWnd; //事件处理窗口
    virtual void OnAccept(int nErrorCode);
};

//CListenSocket实现文件
#include "ListenSocket.h"
CListenSocket::CListenSocket(HWND hWnd){m_hWnd=hWnd;}
void CListenSocket::OnAccept(int nErrorCode)
{
    SendMessage(m_hWnd,WM_SOCKET_MSG,SOCKET_CLNT_ACCEPT,0);
    CSocket::OnAccept(nErrorCode);
}

//主线程
...
m_pListenSocket=new CListenSocket(m_hWnd);
m_pListenSocket->Create(...);
m_pListenSocket->Listen();
...

LRESULT CXxxDlg::OnSocketMsg(WPARAM wParam, LPARAM lParam)
{
    UINT type=(UINT)wParam;
    switch(type)
    {
    case SOCKET_CLNT_ACCEPT:
        {
            CSocket* pSocket=new CSocket;
            if(!m_pListenSocket->Accept(*pSocket))
            {
                delete pSocket;
                break;
            }
            ...
        }
    ...
    }
}


2、用于多线程的时候

常看到人说CSocket在子线程中不能用,其实否则。实际状况是:

直接使用CSocket动态建立的对象,将其指针做为参数传递给子线程,则子线程中进行收发等各类操做都
没问题。但若是是使用CSocket派生类建立的对象,就要看你重载了哪些方法,假如你仅重载了OnClose,
则子线程中你也能够正常收发,但不能Close!

由于CSocket是用内部循环作到同步的,并不依赖各OnXxx,它不须要与CSocketWnd交互。但当你派生并重
载OnXxx后,它为了提供消息机制就必须与CSocketWnd交互。当你调用AfxSocketInit时,你的主线程会获
得一个访问CSocketWnd的句柄,对CSocketWnd的访问是MFC自动帮你完成的,是被隐藏的。而你本身建立
的子线程并不自动具有访问CSocketWnd的机制,因此子线程中须要访问CSocketWnd的操做都会失败。

常看到的解决办法是给子线程传递SOCKET句柄而不是CSocket对象指针,而后在子线程中建立CSocket临时
对象并Attach传入的句柄,用完后再Dettach并delete临时对象。俺没有这么干过,估计是由于Attach方法
含有获取CSocketWnd句柄的内置功能。

俺的解决方案仍是使用自定义消息,好比俺不能在子线程中Close,那么,俺能够给主线程发送一条消息,
让主线程的消息处理函数来完成Close,也很方便。

CSocket通常配合多线程使用,只要你想收发数据,你就能够建立一个CSocket对象,并建立一个子线程来
进行收发。因此被阻塞的只是子线程,而主线程老是能够随时建立子线程去帮它干活。因为可能同时有很
多个CSocket对象在工做,因此你通常还要建立一个列表来储存这些CSocket对象的标识,这样你可能经过
在列表中检索标识来区分各个CSocket对象,固然,因为内存地址的惟一性,对象指针自己就能够做为标识。


相对CAsyncSocket而言,CSocket的运做流程更直观也更简单
相关文章
相关标签/搜索